progmune-runtime 2.1.6 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/README.md +108 -468
  2. package/dist/ablation-study.js +144 -0
  3. package/dist/ablation-study.test.js +18 -0
  4. package/dist/action-runtime.js +3 -1
  5. package/dist/active-learning.js +211 -0
  6. package/dist/analytics.js +139 -0
  7. package/dist/asset-factory.js +309 -0
  8. package/dist/asset-growth.js +244 -0
  9. package/dist/asset-promotion.js +382 -0
  10. package/dist/asset-quality.js +550 -0
  11. package/dist/audit/business-translator.js +285 -0
  12. package/dist/audit/cli.js +66 -0
  13. package/dist/audit/formatters/html.js +379 -0
  14. package/dist/audit/formatters/json.js +11 -0
  15. package/dist/audit/formatters/markdown.js +192 -0
  16. package/dist/audit/formatters/terminal.js +189 -0
  17. package/dist/audit/index.js +25 -0
  18. package/dist/audit/report-builder.js +318 -0
  19. package/dist/audit/types.js +8 -0
  20. package/dist/audit.js +3 -3
  21. package/dist/auto-benchmark-generator.js +137 -0
  22. package/dist/auto-benchmark-generator.test.js +45 -0
  23. package/dist/auto-protocol-synthesizer.js +362 -0
  24. package/dist/auto-protocol-synthesizer.test.js +82 -0
  25. package/dist/autonomous-patch.js +175 -0
  26. package/dist/autonomous-patch.test.js +128 -0
  27. package/dist/badge/badge-server.js +98 -0
  28. package/dist/behavior-miner.js +442 -0
  29. package/dist/belief-layer.js +475 -0
  30. package/dist/benchmark-count.js +5 -0
  31. package/dist/benchmark-generator.js +211 -0
  32. package/dist/benchmark-harness.js +201 -0
  33. package/dist/benchmark-pass-rate.js +7 -0
  34. package/dist/benchmark-report.js +8 -3
  35. package/dist/benchmark-save.js +14 -1
  36. package/dist/bootstrap-validation.js +197 -0
  37. package/dist/bootstrap-validation.test.js +51 -0
  38. package/dist/branch-ledger.js +1 -1
  39. package/dist/capability-gap.js +130 -0
  40. package/dist/certify-html.js +351 -0
  41. package/dist/certify.js +326 -0
  42. package/dist/check.js +4 -4
  43. package/dist/compliance-miner.js +447 -0
  44. package/dist/continuous-benchmark.js +194 -0
  45. package/dist/continuous-benchmark.test.js +116 -0
  46. package/dist/corpus-stats.js +173 -0
  47. package/dist/counterfactual-engine.js +288 -0
  48. package/dist/coverage-dashboard.js +109 -0
  49. package/dist/coverage-system.test.js +205 -0
  50. package/dist/cross-repo-precision.js +352 -0
  51. package/dist/cve-benchmark.js +180 -0
  52. package/dist/cve-benchmark.test.js +28 -0
  53. package/dist/cve-collector.js +73 -0
  54. package/dist/data-quality.js +141 -0
  55. package/dist/decision-engine.js +388 -0
  56. package/dist/derive-metadata.js +250 -0
  57. package/dist/difficulty-active.test.js +198 -0
  58. package/dist/difficulty-map.js +244 -0
  59. package/dist/discovery-analytics.js +125 -0
  60. package/dist/discovery-model.js +149 -0
  61. package/dist/discovery-optimize.test.js +199 -0
  62. package/dist/discovery-trace.js +276 -0
  63. package/dist/discovery-trace.test.js +97 -0
  64. package/dist/emitter.js +83 -1
  65. package/dist/enterprise-dashboard.js +405 -0
  66. package/dist/eval-hardening.js +297 -0
  67. package/dist/eval-hardening.test.js +85 -0
  68. package/dist/evaluation-campaign.js +359 -0
  69. package/dist/evaluation-campaign.test.js +181 -0
  70. package/dist/evidence-growth.js +143 -0
  71. package/dist/evidence-repository.js +209 -0
  72. package/dist/evidence-system.js +441 -0
  73. package/dist/execute.js +15 -7
  74. package/dist/experimental/software-physics.js +291 -0
  75. package/dist/experimental/state-inference.js +516 -0
  76. package/dist/experimental/unsupervised-physics.js +230 -0
  77. package/dist/extract-ir-python.js +54 -7
  78. package/dist/extract-ir.js +376 -12
  79. package/dist/failure-collector.js +2 -2
  80. package/dist/failure-corpus.js +322 -9
  81. package/dist/feedback.js +16 -5
  82. package/dist/feedback.test.js +49 -0
  83. package/dist/file-lock.js +1 -1
  84. package/dist/flywheel-batch.js +292 -0
  85. package/dist/frameworks/express-cli.js +237 -0
  86. package/dist/frameworks/express-detector.js +445 -0
  87. package/dist/frameworks/express-detector.test.js +206 -0
  88. package/dist/frameworks/index.js +30 -0
  89. package/dist/frameworks/nestjs-detector.js +302 -0
  90. package/dist/frameworks/trpc-detector.js +161 -0
  91. package/dist/frameworks/version-awareness.js +179 -0
  92. package/dist/function-synonyms.js +164 -0
  93. package/dist/function-synonyms.test.js +68 -0
  94. package/dist/generalization.test.js +352 -0
  95. package/dist/goal-annotator.js +113 -0
  96. package/dist/goal-planner.js +563 -0
  97. package/dist/gold-cve.js +164 -0
  98. package/dist/gold-cve.test.js +104 -0
  99. package/dist/gold-quality.js +206 -0
  100. package/dist/gold-tiers.js +241 -0
  101. package/dist/governance-dashboard.js +327 -0
  102. package/dist/graph-viz.js +240 -0
  103. package/dist/guided-frontier.js +195 -0
  104. package/dist/hierarchical-planner.js +148 -0
  105. package/dist/identifier-parser.js +260 -0
  106. package/dist/immune-metrics.js +93 -0
  107. package/dist/immune-receiver.js +158 -0
  108. package/dist/immune-reporter.js +1 -1
  109. package/dist/improvement-orchestrator.js +206 -0
  110. package/dist/inject-p0-vocabulary.js +300 -0
  111. package/dist/intent-parser.js +218 -0
  112. package/dist/invariant-algebra.js +476 -0
  113. package/dist/invariant-calculus.js +533 -0
  114. package/dist/ir-utils.js +70 -0
  115. package/dist/ir-utils.test.js +50 -0
  116. package/dist/knowledge-api.js +312 -0
  117. package/dist/knowledge-evolution.js +452 -0
  118. package/dist/knowledge-explorer.js +506 -0
  119. package/dist/knowledge-flywheel.js +274 -0
  120. package/dist/knowledge-governance.js +338 -0
  121. package/dist/knowledge-governance.test.js +150 -0
  122. package/dist/knowledge-graph.js +181 -0
  123. package/dist/knowledge-guided-synth.js +246 -0
  124. package/dist/knowledge-loop.test.js +77 -0
  125. package/dist/knowledge-object.js +316 -0
  126. package/dist/knowledge-package.js +98 -0
  127. package/dist/kpi-dashboard.js +561 -0
  128. package/dist/l3-cross-function.js +280 -0
  129. package/dist/learning-ranker.js +148 -0
  130. package/dist/learning-ranker.test.js +291 -0
  131. package/dist/ledger/accountability.js +322 -0
  132. package/dist/ledger/chain-builder.js +185 -0
  133. package/dist/ledger/cli.js +222 -0
  134. package/dist/ledger/index.js +13 -0
  135. package/dist/ledger/signatures.js +193 -0
  136. package/dist/ledger/types.js +9 -0
  137. package/dist/llm.js +74 -3
  138. package/dist/load-benchmarks.js +8 -3
  139. package/dist/logger.js +66 -0
  140. package/dist/logger.test.js +37 -0
  141. package/dist/logistic-reward.js +339 -0
  142. package/dist/logistic-reward.test.js +180 -0
  143. package/dist/macro-graph.js +193 -0
  144. package/dist/macro-repair.js +183 -0
  145. package/dist/mcp-server.mjs +1202 -483
  146. package/dist/memory-layer.js +42 -5
  147. package/dist/multi-repo-precision.js +422 -0
  148. package/dist/name-free-protocol.js +425 -0
  149. package/dist/name-free-protocol.test.js +170 -0
  150. package/dist/name-scrambling.js +138 -0
  151. package/dist/name-scrambling.test.js +16 -0
  152. package/dist/p3-observability.test.js +281 -0
  153. package/dist/p5-orchestrator.test.js +225 -0
  154. package/dist/pairwise-preference.js +294 -0
  155. package/dist/pairwise-preference.test.js +140 -0
  156. package/dist/planner-constraints.js +104 -0
  157. package/dist/planner-prompts.js +155 -0
  158. package/dist/planner-telemetry.js +415 -0
  159. package/dist/planner-trace.js +214 -0
  160. package/dist/planner.js +162 -167
  161. package/dist/plsb/artifact.js +116 -0
  162. package/dist/plsb/cli.js +71 -0
  163. package/dist/plsb/index.js +19 -0
  164. package/dist/plsb/leaderboard.js +249 -0
  165. package/dist/plsb/report-md.js +156 -0
  166. package/dist/plsb/schema.js +179 -0
  167. package/dist/plsb-benchmark.js +284 -0
  168. package/dist/plsb-benchmark.test.js +119 -0
  169. package/dist/policy/cli.js +134 -0
  170. package/dist/policy/engine.js +333 -0
  171. package/dist/policy/index.js +12 -0
  172. package/dist/policy/types.js +59 -0
  173. package/dist/policy-miner.js +505 -0
  174. package/dist/precision-analyze.js +229 -0
  175. package/dist/precision-benchmark.js +147 -0
  176. package/dist/precision-label-c.js +134 -0
  177. package/dist/precision-label.js +193 -0
  178. package/dist/precision-report-c.js +149 -0
  179. package/dist/precision-report.js +246 -0
  180. package/dist/progmune-status.js +108 -0
  181. package/dist/proof-engine.js +479 -0
  182. package/dist/proof-provenance.js +315 -0
  183. package/dist/protocol-coverage.js +294 -0
  184. package/dist/protocol-detector.js +1189 -0
  185. package/dist/protocol-embedding-expanded.js +297 -0
  186. package/dist/protocol-embedding-expanded.test.js +97 -0
  187. package/dist/protocol-embedding.js +195 -0
  188. package/dist/protocol-embedding.test.js +82 -0
  189. package/dist/protocol-extractor-v2.js +354 -0
  190. package/dist/protocol-extractor-v2.test.js +140 -0
  191. package/dist/protocol-extractor.js +310 -0
  192. package/dist/protocol-extractor.test.js +113 -0
  193. package/dist/protocol-foundation.js +322 -0
  194. package/dist/protocol-foundation.test.js +163 -0
  195. package/dist/protocol-frontier.js +243 -0
  196. package/dist/protocol-frontier.test.js +92 -0
  197. package/dist/protocol-gap-analyzer.js +228 -0
  198. package/dist/protocol-gap-analyzer.test.js +49 -0
  199. package/dist/protocol-invariants.js +276 -0
  200. package/dist/protocol-invariants.test.js +111 -0
  201. package/dist/protocol-knowledge.js +464 -0
  202. package/dist/protocol-miner.js +343 -0
  203. package/dist/protocol-mining.js +207 -0
  204. package/dist/protocol-mining.test.js +37 -0
  205. package/dist/protocol-registry.js +1 -1
  206. package/dist/protocol-security-benchmark.js +222 -0
  207. package/dist/protocol-vulnerability.js +257 -0
  208. package/dist/protocol-vulnerability.test.js +60 -0
  209. package/dist/python-benchmark.js +120 -0
  210. package/dist/python-emitter.js +163 -45
  211. package/dist/python-protocol-extractor.js +187 -0
  212. package/dist/python-protocol-extractor.test.js +116 -0
  213. package/dist/realworld-benchmark.js +646 -0
  214. package/dist/realworld-benchmark.test.js +36 -0
  215. package/dist/repair-arch.test.js +411 -0
  216. package/dist/repair-evolution.test.js +454 -0
  217. package/dist/repair-executor.js +719 -0
  218. package/dist/repair-proposal.js +4 -4
  219. package/dist/repair-ranker.js +141 -0
  220. package/dist/repair-strategies.js +419 -0
  221. package/dist/repair-taxonomy.js +234 -0
  222. package/dist/repair-types.js +12 -0
  223. package/dist/repo-evaluator.js +250 -0
  224. package/dist/repo-evaluator.test.js +128 -0
  225. package/dist/resource-abstraction.js +242 -0
  226. package/dist/resource-detector.js +211 -0
  227. package/dist/result.test.js +43 -0
  228. package/dist/reward-system.js +411 -0
  229. package/dist/reward-system.test.js +175 -0
  230. package/dist/risk-model.js +215 -0
  231. package/dist/rule-miner.js +234 -7
  232. package/dist/rule-specificity.js +254 -0
  233. package/dist/runtime-types.js +27 -0
  234. package/dist/scaffold.js +208 -0
  235. package/dist/scale-collector.test.js +101 -0
  236. package/dist/scale-trajectory-collector.js +128 -0
  237. package/dist/sdk.js +250 -0
  238. package/dist/search-planner.js +4 -41
  239. package/dist/semantic-snapshot.js +1 -1
  240. package/dist/semantic-topology.js +121 -0
  241. package/dist/semantic-trace.js +310 -317
  242. package/dist/sequence-extractor.js +343 -0
  243. package/dist/skill-library.js +245 -0
  244. package/dist/skill-planner.test.js +189 -0
  245. package/dist/software-physics.js +291 -0
  246. package/dist/software-physics.test.js +81 -0
  247. package/dist/ssg-precision.js +478 -0
  248. package/dist/ssg-validator.js +71 -21
  249. package/dist/state-inference-doubleblind.test.js +160 -0
  250. package/dist/state-inference.js +516 -0
  251. package/dist/state-inference.test.js +115 -0
  252. package/dist/state-machine-fingerprint.js +345 -0
  253. package/dist/state-machine-fingerprint.test.js +120 -0
  254. package/dist/state-miner.js +386 -0
  255. package/dist/state-name-inference.js +213 -0
  256. package/dist/state-name-inference.test.js +69 -0
  257. package/dist/strategy-planner.js +262 -96
  258. package/dist/strategy-planner.test.js +135 -0
  259. package/dist/telemetry-analytics.test.js +402 -0
  260. package/dist/terminal-format.js +68 -0
  261. package/dist/terminal-format.test.js +83 -0
  262. package/dist/topology-factory.js +196 -0
  263. package/dist/topology-representation.js +242 -0
  264. package/dist/topology-representation.test.js +27 -0
  265. package/dist/trajectory-augmentation.js +254 -0
  266. package/dist/trajectory-augmentation.test.js +63 -0
  267. package/dist/trajectory-corpus.js +440 -0
  268. package/dist/trajectory-corpus.test.js +32 -0
  269. package/dist/trajectory-feedback.test.js +116 -0
  270. package/dist/transition-synthesizer.js +286 -0
  271. package/dist/transition-synthesizer.test.js +123 -0
  272. package/dist/trust/api-semantic-mapper.js +809 -0
  273. package/dist/trust/call-graph-propagator.js +225 -0
  274. package/dist/trust/cli.js +122 -0
  275. package/dist/trust/compliance-scorer.js +283 -0
  276. package/dist/trust/confidence-calculator.js +261 -0
  277. package/dist/trust/engine.js +1145 -0
  278. package/dist/trust/explainability.js +85 -0
  279. package/dist/trust/formatters/ci.js +42 -0
  280. package/dist/trust/formatters/json.js +11 -0
  281. package/dist/trust/formatters/terminal.js +152 -0
  282. package/dist/trust/index.js +39 -0
  283. package/dist/trust/phase1-verify.js +171 -0
  284. package/dist/trust/protocol-domain-validator.js +697 -0
  285. package/dist/trust/score-calculator.js +282 -0
  286. package/dist/trust/ssg-bridge.js +641 -0
  287. package/dist/trust/ssg-bridge.test.js +269 -0
  288. package/dist/trust/types.js +67 -0
  289. package/dist/trust/violation-trace.js +335 -0
  290. package/dist/trust-api.js +179 -0
  291. package/dist/trust-calibration.js +279 -0
  292. package/dist/unknown-protocol-discovery.js +339 -0
  293. package/dist/unknown-protocol-discovery.test.js +102 -0
  294. package/dist/unsupervised-physics.js +230 -0
  295. package/dist/unsupervised-physics.test.js +95 -0
  296. package/dist/utils.test.js +37 -0
  297. package/dist/validator.js +187 -10
  298. package/dist/verification-intelligence.js +475 -0
  299. package/dist/verify-api.js +432 -0
  300. package/dist/vi-impact-report.js +293 -0
  301. package/dist/wl-fingerprint.js +162 -0
  302. package/dist/wl-fingerprint.test.js +130 -0
  303. package/dist/zeroshot-strategy.js +139 -0
  304. package/docs/Progmune_/346/212/225/350/265/204/344/272/272/347/231/275/347/232/256/344/271/246_v2.0.html +576 -0
  305. package/docs/Progmune_/351/241/271/347/233/256/345/205/250/350/247/243.html +710 -0
  306. package/package.json +74 -7
  307. package/protocols.json +1956 -50
  308. package/.dockerignore +0 -14
  309. package/.mcp.json +0 -11
  310. package/.progmune_allowlist +0 -50
  311. package/.test_report/test_report.md +0 -87
  312. package/Dockerfile +0 -9
  313. package/FAQ.md +0 -167
  314. package/WHITEPAPER.md +0 -540
  315. package/demo-project/auth.ts +0 -55
  316. package/demo-project/tsconfig.json +0 -8
  317. package/dist/acl-breakdown.js +0 -13
  318. package/dist/all-sessions.js +0 -11
  319. package/dist/antibody-stats.js +0 -11
  320. package/dist/branch-tree-count.js +0 -14
  321. package/dist/common-fixpath.js +0 -12
  322. package/dist/constraint-types.js +0 -12
  323. package/dist/exec-metrics.js +0 -11
  324. package/dist/failure-report.js +0 -11
  325. package/dist/fast-path-hits.js +0 -13
  326. package/dist/fingerprint-list.js +0 -15
  327. package/dist/gen-history-log.js +0 -13
  328. package/dist/heatmap-data.js +0 -11
  329. package/dist/recent-session.js +0 -12
  330. package/dist/svl-distribution.js +0 -11
  331. package/dist/terminal-status.js +0 -11
  332. package/dist/token-savings.js +0 -11
  333. package/dist/total-repairs.js +0 -12
  334. package/dist/unresolved-count.js +0 -12
  335. package/dist/valid-fingerprints.js +0 -13
  336. package/dist/verify-ledgers.js +0 -11
  337. package/docs/whitepaper-style.css +0 -77
  338. package/docs/whitepaper-v2.1.md +0 -609
  339. package/docs/whitepaper-v2.2.md +0 -1064
  340. package/docs/whitepaper-v2.2.pdf +0 -0
  341. package/fly.toml +0 -31
  342. package/public/dashboard.html +0 -119
  343. package/server/hub.js +0 -116
  344. package/test/replay-golden/sess_1780063202050_mgeld.json +0 -9
  345. package/test/replay-golden/sess_1780064032560_gocld.json +0 -354
  346. package/test/replay-golden/sess_1780064413331_s2709.json +0 -606
  347. package/test/replay-golden/sess_1780064792710_y3avo.json +0 -614
  348. package/test/replay-golden.ts +0 -84
  349. package/test_benchmark.js +0 -165
  350. package/test_comprehensive.mjs +0 -638
  351. package/test_concurrency.js +0 -129
  352. package/test_ir_robustness.js +0 -85
  353. package/test_semantic_contracts.js +0 -269
  354. package/test_ssg_stress.js +0 -156
  355. package/test_svl3.js +0 -58
  356. package/tsconfig.json +0 -17
@@ -14,12 +14,20 @@ exports.formatChainHint = formatChainHint;
14
14
  const utils_1 = require("./utils");
15
15
  const feedback_1 = require("./feedback");
16
16
  const semantic_topology_1 = require("./semantic-topology");
17
+ const planner_constraints_1 = require("./planner-constraints");
18
+ const failure_corpus_1 = require("./failure-corpus");
17
19
  /** Build a capability graph from IR functions. */
18
20
  function buildCapabilityGraph(ir) {
21
+ const SKIP_FILES = new Set(["src/strategy-planner.ts", "src/planner.ts"]);
22
+ const SKIP_TAGS = new Set(); // P8.3: Python IR now supported
19
23
  const graph = new Map();
20
24
  for (const f of ir) {
21
25
  if (!f.exported)
22
26
  continue;
27
+ if (SKIP_FILES.has(f.file))
28
+ continue;
29
+ if ((f.tags || []).some(t => SKIP_TAGS.has(t)))
30
+ continue; // skip language-specific
23
31
  graph.set(f.name, {
24
32
  name: f.name,
25
33
  purpose: f.purpose || "",
@@ -28,6 +36,8 @@ function buildCapabilityGraph(ir) {
28
36
  produces: f.produces || [],
29
37
  useWhen: f.useWhen || [],
30
38
  score: 0,
39
+ hasExplicitMeta: !f._requiresDerived && !f._producesDerived
40
+ && (f.requires || []).length > 0 && (f.produces || []).length > 0,
31
41
  });
32
42
  }
33
43
  return graph;
@@ -37,26 +47,54 @@ function buildCapabilityGraph(ir) {
37
47
  function scoreNode(node, intentLower, keywords) {
38
48
  let score = 0;
39
49
  let hasMatch = false;
50
+ // ── Primary verb detection: first keyword is the main action ──
51
+ const primaryVerb = keywords.length > 0 ? keywords[0] : "";
52
+ const nameWords = node.name.replace(/([A-Z])/g, " $1").toLowerCase().split(/[\s_]+/).filter(w => w.length > 1);
53
+ const nameLower = node.name.toLowerCase();
40
54
  // Name match
41
55
  for (const kw of keywords) {
42
- if (node.name.toLowerCase().includes(kw)) {
56
+ // Substring match
57
+ if (nameLower.includes(kw)) {
43
58
  score += 1;
44
59
  hasMatch = true;
60
+ // Exact word match
61
+ if (nameWords.includes(kw)) {
62
+ // Primary verb (first keyword) that STARTS the function name → decisive bonus
63
+ // e.g., "extract" matches "extractIR" → +5, beats generic "validate" matches
64
+ if (kw === primaryVerb && nameLower.startsWith(kw))
65
+ score += 5;
66
+ else
67
+ score += 2;
68
+ }
45
69
  }
46
- const js = (0, utils_1.jaccardSimilarity)(node.name.toLowerCase(), kw);
70
+ const js = (0, utils_1.jaccardSimilarity)(nameLower, kw);
47
71
  if (js > 0.2) {
48
72
  score += js;
49
73
  hasMatch = true;
50
74
  }
51
75
  }
52
- // Purpose match
76
+ // Purpose match — semantic bridge when function names differ from intent
53
77
  const purposeLower = node.purpose.toLowerCase();
78
+ const intentWords = intentLower.split(/[\s,,]+/).filter(w => w.length > 2);
54
79
  for (const kw of keywords) {
55
80
  if (purposeLower.includes(kw)) {
56
81
  score += 2;
57
82
  hasMatch = true;
58
83
  }
59
84
  }
85
+ // Jaccard purpose similarity: "send receipt" ↔ "dispatch a message to recipient"
86
+ if (purposeLower.length > 0) {
87
+ const purposeWords = purposeLower.split(/[\s,,]+/).filter(w => w.length > 2);
88
+ if (purposeWords.length > 0 && intentWords.length > 0) {
89
+ const shared = intentWords.filter(w => purposeWords.includes(w)).length;
90
+ const union = new Set([...intentWords, ...purposeWords]).size;
91
+ const purposeJaccard = shared / union;
92
+ if (purposeJaccard > 0.1) {
93
+ score += purposeJaccard * 5;
94
+ hasMatch = true;
95
+ }
96
+ }
97
+ }
60
98
  // Tag match
61
99
  for (const tag of node.tags) {
62
100
  if (intentLower.includes(tag.toLowerCase())) {
@@ -65,7 +103,6 @@ function scoreNode(node, intentLower, keywords) {
65
103
  }
66
104
  }
67
105
  // Semantic word overlap in purpose
68
- const intentWords = intentLower.split(/[\s,,]+/);
69
106
  for (const w of intentWords) {
70
107
  if (w.length > 2 && purposeLower.includes(w)) {
71
108
  score += 0.5;
@@ -90,66 +127,64 @@ function scoreNode(node, intentLower, keywords) {
90
127
  // Require at least one match to be relevant
91
128
  if (!hasMatch)
92
129
  return 0;
93
- // Dynamic Credit: multiply by actual success rate
130
+ // Bonus: explicit annotations are more reliable than derived ones
131
+ if (node.hasExplicitMeta)
132
+ score *= 1.3;
133
+ // L3: Dynamic Credit — multiply by actual success rate
94
134
  const successRate = (0, feedback_1.getFailureAdjustedCredit)(node.name);
95
135
  const creditFactor = 0.3 + successRate * 0.7;
96
- return score * creditFactor;
136
+ score *= creditFactor;
137
+ // Rule → Planner: apply mined rule constraints
138
+ const constraints = (0, planner_constraints_1.getConstraints)();
139
+ const { multiplier } = (0, planner_constraints_1.applyConstraints)(node.name, node.purpose, constraints);
140
+ return score * multiplier;
97
141
  }
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) {
101
- const producers = [];
142
+ /** Pick the highest-scoring node from a list. */
143
+ function topByScore(nodes) {
144
+ return nodes.sort((a, b) => b.score - a.score)[0];
145
+ }
146
+ /** Shared logic for finding nodes that produce or consume a capability label.
147
+ * Falls back to topology similarity if no direct data-flow match.
148
+ *
149
+ * @param field - which side of the data-flow edge to match on
150
+ * @param isProducer - when true the field value acts as the "produce" side
151
+ * of capabilityMatch; when false it acts as the "require" side. */
152
+ function findRelated(graph, capability, allNodes, field, isProducer) {
153
+ const results = [];
102
154
  for (const node of graph.values()) {
103
- if (node.produces.some(p => p === capability || capability.includes(p) || p.includes(capability))) {
104
- producers.push(node);
155
+ if (node[field].some(v => v === capability || capability.includes(v) || v.includes(capability))) {
156
+ results.push(node);
105
157
  }
106
158
  }
107
- // Topology fallback: find semantically related producers
108
- if (producers.length === 0) {
159
+ // Topology fallback: find semantically related nodes via capabilityMatch
160
+ if (results.length === 0) {
109
161
  try {
110
162
  const topo = (0, semantic_topology_1.getTopology)();
111
163
  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);
164
+ if (node[field].length > 0) {
165
+ for (const v of node[field]) {
166
+ const matched = isProducer
167
+ ? topo.capabilityMatch(v, capability)
168
+ : topo.capabilityMatch(capability, v);
169
+ if (matched && !results.includes(node)) {
170
+ results.push(node);
116
171
  break;
117
172
  }
118
173
  }
119
174
  }
120
175
  }
121
176
  }
122
- catch { }
177
+ catch { /* topology fallback unavailable — non-critical */ }
123
178
  }
124
- return producers;
179
+ return results;
125
180
  }
126
- /** Find all capability nodes that require a given capability label.
127
- * Falls back to topology similarity if no direct data-flow match. */
181
+ /** Find all capability nodes that produce a given capability label. */
182
+ function findProducers(graph, capability, allNodes) {
183
+ return findRelated(graph, capability, allNodes, "produces", true);
184
+ }
185
+ /** Find all capability nodes that require a given capability label. */
128
186
  function findConsumers(graph, capability, allNodes) {
129
- const consumers = [];
130
- for (const node of graph.values()) {
131
- if (node.requires.some(r => r === capability || capability.includes(r) || r.includes(capability))) {
132
- consumers.push(node);
133
- }
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
- }
152
- return consumers;
187
+ return findRelated(graph, capability, allNodes, "requires", false);
153
188
  }
154
189
  /**
155
190
  * Select the best capability chain for an intent.
@@ -160,11 +195,19 @@ function findConsumers(graph, capability, allNodes) {
160
195
  * 3. For each producer, trace forward: producer → consumer → consumer...
161
196
  * 4. Score each chain and return top N
162
197
  */
163
- function selectCapabilityChains(intent, ir, maxChains = 5) {
198
+ function selectCapabilityChains(intent, ir, maxChains = 5, llmSeeds) {
199
+ // Guard: null/undefined/empty intent
200
+ if (!intent || typeof intent !== "string" || !intent.trim())
201
+ return { chains: [], needsLLM: false };
202
+ // Ensure semantic topology is built (cached after first build)
203
+ const startTopo = Date.now();
204
+ (0, semantic_topology_1.getTopology)(ir);
205
+ const topoMs = Date.now() - startTopo;
164
206
  const intentLower = intent.toLowerCase();
165
207
  const keywords = (0, utils_1.extractKeywords)(intent);
166
208
  const graph = buildCapabilityGraph(ir);
167
209
  // Score all nodes
210
+ const startScore = Date.now();
168
211
  for (const node of graph.values()) {
169
212
  node.score = scoreNode(node, intentLower, keywords);
170
213
  }
@@ -174,75 +217,180 @@ function selectCapabilityChains(intent, ir, maxChains = 5) {
174
217
  let seeds = [...graph.values()]
175
218
  .filter(n => n.score > dynamicThreshold && (n.produces.length > 0 || n.score > dynamicThreshold + 2))
176
219
  .sort((a, b) => b.score - a.score);
177
- if (seeds.length === 0 && dynamicThreshold > 0.5) {
220
+ if (seeds.length === 0 && dynamicThreshold > 0.3) {
178
221
  dynamicThreshold *= 0.5;
179
222
  seeds = [...graph.values()]
180
223
  .filter(n => n.score > dynamicThreshold && (n.produces.length > 0 || n.score > dynamicThreshold + 1))
181
224
  .sort((a, b) => b.score - a.score);
182
225
  }
183
- seeds = seeds.slice(0, graph.size > 500 ? 30 : 15);
226
+ // Best-effort fallback: prefer nodes with edges AND some score
227
+ if (seeds.length === 0) {
228
+ seeds = [...graph.values()]
229
+ .filter(n => n.score > 0 && (n.produces.length > 0 || n.requires.length > 0))
230
+ .sort((a, b) => b.score - a.score)
231
+ .slice(0, 5);
232
+ }
233
+ // Ultimate fallback: just take any exported nodes with score > 0
234
+ if (seeds.length === 0) {
235
+ seeds = [...graph.values()].filter(n => n.score > 0).slice(0, 3);
236
+ }
237
+ seeds = seeds.slice(0, graph.size > 1000 ? 10 : graph.size > 500 ? 20 : 15);
238
+ // ── Capability Ranking: preload edge confidence from session history ──
239
+ const edgeConfMap = new Map();
240
+ for (const ec of (0, failure_corpus_1.getEdgeConfidence)()) {
241
+ edgeConfMap.set(`${ec.producer}→${ec.consumer}`, ec.confidence);
242
+ }
243
+ // ── LLM seed injection: bridge the semantic gap ──
244
+ if (llmSeeds && llmSeeds.length > 0) {
245
+ const seedNames = new Set(seeds.map(n => n.name));
246
+ const maxScore = seeds.length > 0 ? Math.max(...seeds.map(n => n.score)) : 1;
247
+ for (const fnName of llmSeeds) {
248
+ if (seedNames.has(fnName))
249
+ continue;
250
+ const node = graph.get(fnName);
251
+ if (node) {
252
+ node.score = Math.max(node.score, maxScore);
253
+ seeds.unshift(node);
254
+ seedNames.add(fnName); // keep seedNames in sync for edge expansion
255
+ }
256
+ }
257
+ // Edge Confidence expansion: high-conf downstream from seeds
258
+ let edgeAdded = 0;
259
+ for (const [edgeKey, conf] of edgeConfMap) {
260
+ if (conf < 0.9)
261
+ continue;
262
+ const [prod, cons] = edgeKey.split("→");
263
+ if (!seedNames.has(prod))
264
+ continue;
265
+ if (seedNames.has(cons))
266
+ continue;
267
+ const consumerNode = graph.get(cons);
268
+ if (consumerNode) {
269
+ consumerNode.score = Math.max(consumerNode.score, maxScore * 0.8);
270
+ seeds.push(consumerNode);
271
+ edgeAdded++;
272
+ }
273
+ }
274
+ if (edgeAdded > 0)
275
+ console.error(`[EdgeExp] ${edgeAdded}高置信边扩展种子 (+${edgeAdded} nodes)`);
276
+ }
184
277
  const allNodes = [...graph.values()];
185
278
  const chains = [];
279
+ const BEAM_WIDTH = graph.size > 500 ? 3 : 5;
280
+ const MAX_CHAIN_LEN = parseInt(process.env.PROGMUNE_MAX_CHAIN_LEN || "5", 10);
281
+ const SCORE_FLOOR = graph.size > 1000 ? 0.2 : 0.1; // always filter noise
186
282
  for (const seed of seeds) {
187
- // Build chain: seed → consumer → consumer...
188
- const chain = [seed];
189
- const visited = new Set([seed.name]);
190
- let totalScore = seed.score;
191
- // Forward trace: data flow → semantic leap
192
- let current = seed;
193
- let extended = true;
194
- let leapDecay = 1.0; // weight decay for semantic leaps
195
- while (extended && chain.length < 8) {
196
- extended = false;
197
- // Strategy 1: direct data flow (produces → requires)
198
- for (const p of current.produces) {
199
- const consumers = findConsumers(graph, p, allNodes).filter(c => !visited.has(c.name));
200
- if (consumers.length > 0) {
201
- const bestConsumer = consumers.sort((a, b) => b.score - a.score)[0];
202
- chain.push(bestConsumer);
203
- visited.add(bestConsumer.name);
204
- totalScore += bestConsumer.score;
205
- current = bestConsumer;
206
- extended = true;
207
- leapDecay = 1.0; // reset decay on direct match
208
- break;
283
+ let beam = [{
284
+ chain: [seed],
285
+ visited: new Set([seed.name]),
286
+ totalScore: seed.score,
287
+ leapDecay: 1.0,
288
+ deadEnd: false,
289
+ }];
290
+ let bestEntry = null;
291
+ for (let depth = 0; depth < MAX_CHAIN_LEN && beam.length > 0; depth++) {
292
+ const nextBeam = [];
293
+ for (const entry of beam) {
294
+ if (entry.deadEnd || entry.chain.length >= MAX_CHAIN_LEN) {
295
+ // Dead-end or max length: finalize this branch
296
+ if (!bestEntry || (entry.totalScore / entry.chain.length) > (bestEntry.totalScore / bestEntry.chain.length)) {
297
+ bestEntry = entry;
298
+ }
299
+ continue;
209
300
  }
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
301
+ const current = entry.chain[entry.chain.length - 1];
302
+ let expanded = false;
303
+ // Strategy 1: direct data flow (produces → requires)
304
+ for (const p of current.produces) {
305
+ const consumers = findConsumers(graph, p, allNodes)
306
+ .filter(c => !entry.visited.has(c.name) && c.score >= SCORE_FLOOR);
307
+ for (const consumer of consumers) {
308
+ const newVisited = new Set(entry.visited);
309
+ newVisited.add(consumer.name);
310
+ // Capability Ranking: edge confidence × consumer score
311
+ const edgeKey = `${current.name}→${consumer.name}`;
312
+ const edgeConf = edgeConfMap.get(edgeKey) ?? 0.5; // Laplace prior for unseen edges
313
+ const rankedScore = consumer.score * (0.5 + edgeConf * 0.5);
314
+ nextBeam.push({
315
+ chain: [...entry.chain, consumer],
316
+ visited: newVisited,
317
+ totalScore: entry.totalScore + rankedScore,
318
+ leapDecay: 1.0,
319
+ deadEnd: consumer.score === 0 && consumer.produces.length === 0,
320
+ });
321
+ expanded = true;
322
+ }
323
+ }
324
+ // Strategy 2: semantic leap — topology similarity
325
+ if (!expanded) {
326
+ try {
327
+ const topo = (0, semantic_topology_1.getTopology)();
328
+ const similar = topo.findSimilar(current.name, 10)
329
+ .filter(s => !entry.visited.has(s.name) && s.similarity > 0.25);
330
+ for (const s of similar.slice(0, 3)) {
331
+ const bestMatch = graph.get(s.name);
332
+ if (bestMatch && bestMatch.score >= SCORE_FLOOR) {
333
+ const newVisited = new Set(entry.visited);
334
+ newVisited.add(bestMatch.name);
335
+ nextBeam.push({
336
+ chain: [...entry.chain, bestMatch],
337
+ visited: newVisited,
338
+ totalScore: entry.totalScore + bestMatch.score * entry.leapDecay,
339
+ leapDecay: entry.leapDecay * 0.7, // decay 30% per leap
340
+ deadEnd: false,
341
+ });
342
+ expanded = true;
343
+ }
226
344
  }
227
345
  }
346
+ catch { /* semantic leap unavailable — non-critical */ }
347
+ }
348
+ // Dead-end: no consumers found → finalize this branch
349
+ if (!expanded) {
350
+ if (!bestEntry || (entry.totalScore / entry.chain.length) > (bestEntry.totalScore / bestEntry.chain.length)) {
351
+ bestEntry = entry;
352
+ }
228
353
  }
229
- catch { }
230
354
  }
355
+ // Beam pruning: keep top BEAM_WIDTH by average score
356
+ nextBeam.sort((a, b) => (b.totalScore / b.chain.length) - (a.totalScore / a.chain.length));
357
+ beam = nextBeam.slice(0, BEAM_WIDTH);
231
358
  }
359
+ // Any remaining beam entries compete for best
360
+ for (const entry of beam) {
361
+ if (!bestEntry || (entry.totalScore / entry.chain.length) > (bestEntry.totalScore / bestEntry.chain.length)) {
362
+ bestEntry = entry;
363
+ }
364
+ }
365
+ if (!bestEntry || bestEntry.chain.length < 1)
366
+ continue;
367
+ const chain = bestEntry.chain;
368
+ let totalScore = bestEntry.totalScore;
369
+ const visited = bestEntry.visited;
232
370
  // Backward trace: does seed need something? Find producers.
233
371
  if (seed.requires.length > 0) {
234
372
  const producers = findProducers(graph, seed.requires[0], allNodes)
235
373
  .filter(p => !visited.has(p.name));
236
374
  if (producers.length > 0) {
237
- const bestProducer = producers.sort((a, b) => b.score - a.score)[0];
375
+ const bestProducer = topByScore(producers);
238
376
  chain.unshift(bestProducer);
239
377
  totalScore += bestProducer.score;
240
378
  }
241
379
  }
380
+ // Hybrid: supplement graph chain with high-score keyword matches the graph missed
381
+ const missed = allNodes
382
+ .filter(n => !visited.has(n.name) && n.score >= SCORE_FLOOR)
383
+ .sort((a, b) => b.score - a.score)
384
+ .slice(0, 2); // at most 2 supplements
385
+ for (const m of missed) {
386
+ chain.push(m);
387
+ visited.add(m.name);
388
+ totalScore += m.score * 0.5; // half weight — graph-discovered nodes are preferred
389
+ }
242
390
  if (chain.length >= 1) {
243
391
  chains.push({
244
392
  nodes: chain,
245
- score: totalScore / chain.length, // average score
393
+ score: totalScore / chain.length,
246
394
  explanation: chain.map(n => n.name).join(" → "),
247
395
  });
248
396
  }
@@ -258,16 +406,34 @@ function selectCapabilityChains(intent, ir, maxChains = 5) {
258
406
  unique.push(c);
259
407
  }
260
408
  }
261
- return unique.slice(0, maxChains);
409
+ const scoreMs = Date.now() - startScore;
410
+ // Detect semantic gap: very few seeds passed threshold → LLM could help
411
+ const needsLLM = seeds.length === 0 || (seeds.length <= 2 && graph.size > 50);
412
+ if (graph.size > 200) {
413
+ console.error(`[Strategy] ${graph.size} nodes | topo=${topoMs}ms score=${scoreMs}ms | ${unique.length} chains | needsLLM=${needsLLM}`);
414
+ }
415
+ return { chains: unique.slice(0, maxChains), needsLLM };
262
416
  }
263
- /** Format chains as a hint for the LLM prompt. */
417
+ /** Format chains as a routing hint (not full path).
418
+ * Only shows top 2 high-confidence next steps per entry,
419
+ * so the LLM gets guidance without being forced into long chains. */
264
420
  function formatChainHint(chains) {
265
421
  if (chains.length === 0)
266
422
  return "";
267
- const lines = ["\n推荐能力链 (Strategy Layer):"];
268
- for (let i = 0; i < Math.min(chains.length, 3); i++) {
269
- const c = chains[i];
270
- lines.push(` ${i + 1}. ${c.explanation} (★${c.score.toFixed(1)})`);
423
+ const lines = ["\n建议的下一步调用 (已从历史成功链中验证):"];
424
+ const shown = new Set();
425
+ for (const c of chains.slice(0, 3)) {
426
+ const pairs = [];
427
+ for (let i = 0; i < c.nodes.length - 1 && pairs.length < 2; i++) {
428
+ const key = `${c.nodes[i].name}→${c.nodes[i + 1].name}`;
429
+ if (!shown.has(key)) {
430
+ shown.add(key);
431
+ pairs.push(`${c.nodes[i].name} → ${c.nodes[i + 1].name}`);
432
+ }
433
+ }
434
+ if (pairs.length > 0) {
435
+ lines.push(` ${pairs.join(" | ")} (★${c.score.toFixed(1)})`);
436
+ }
271
437
  }
272
438
  return lines.join("\n");
273
439
  }
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Unit tests for strategy-planner pure functions.
5
+ * Tests the graph-building and chain-selection logic in isolation.
6
+ */
7
+ const vitest_1 = require("vitest");
8
+ const strategy_planner_1 = require("./strategy-planner");
9
+ // Minimal mock IR with functions covering auth and data domains
10
+ const mockIR = [
11
+ {
12
+ name: "validatePassword",
13
+ purpose: "Validate a user's password against stored hash",
14
+ tags: ["auth", "security"],
15
+ requires: ["password", "hash"],
16
+ produces: ["validation_result"],
17
+ useWhen: ["user login", "password reset"],
18
+ file: "src/auth.ts",
19
+ exported: true,
20
+ params: [
21
+ { name: "password", type: "string" },
22
+ { name: "hash", type: "string" },
23
+ ],
24
+ returnType: "boolean",
25
+ },
26
+ {
27
+ name: "generateJWT",
28
+ purpose: "Generate a JWT token for authenticated user",
29
+ tags: ["auth", "token"],
30
+ requires: ["validation_result"],
31
+ produces: ["jwt_token"],
32
+ useWhen: ["after successful login"],
33
+ file: "src/auth.ts",
34
+ exported: true,
35
+ params: [{ name: "userId", type: "string" }],
36
+ returnType: "string",
37
+ },
38
+ {
39
+ name: "fetchUser",
40
+ purpose: "Fetch user data from the database",
41
+ tags: ["data", "user"],
42
+ requires: ["userId"],
43
+ produces: ["user_data"],
44
+ useWhen: ["user profile", "dashboard"],
45
+ file: "src/data.ts",
46
+ exported: true,
47
+ params: [{ name: "userId", type: "string" }],
48
+ returnType: "object",
49
+ },
50
+ {
51
+ name: "saveToDatabase",
52
+ purpose: "Save record to persistent storage",
53
+ tags: ["data", "storage"],
54
+ requires: ["record"],
55
+ produces: ["saved_id"],
56
+ useWhen: ["data persistence"],
57
+ file: "src/data.ts",
58
+ exported: true,
59
+ params: [{ name: "record", type: "object" }],
60
+ returnType: "string",
61
+ },
62
+ // Non-exported function — should be skipped
63
+ {
64
+ name: "internalHelper",
65
+ purpose: "Internal helper — not exported",
66
+ tags: [],
67
+ requires: [],
68
+ produces: [],
69
+ file: "src/internal.ts",
70
+ exported: false,
71
+ params: [],
72
+ returnType: "void",
73
+ },
74
+ ];
75
+ (0, vitest_1.describe)("selectCapabilityChains", () => {
76
+ (0, vitest_1.it)("returns chains for an auth intent", () => {
77
+ const { chains } = (0, strategy_planner_1.selectCapabilityChains)("authenticate user", mockIR, 3);
78
+ (0, vitest_1.expect)(chains.length).toBeGreaterThan(0);
79
+ // Should find at least validatePassword or generateJWT
80
+ const names = chains.flatMap(c => c.nodes.map(n => n.name));
81
+ (0, vitest_1.expect)(names.some(n => n.includes("validate") || n.includes("JWT"))).toBe(true);
82
+ });
83
+ (0, vitest_1.it)("returns chains for a data intent", () => {
84
+ const { chains } = (0, strategy_planner_1.selectCapabilityChains)("fetch user data", mockIR, 3);
85
+ (0, vitest_1.expect)(chains.length).toBeGreaterThan(0);
86
+ const names = chains.flatMap(c => c.nodes.map(n => n.name));
87
+ (0, vitest_1.expect)(names.some(n => n.includes("fetchUser"))).toBe(true);
88
+ });
89
+ (0, vitest_1.it)("respects maxChains limit", () => {
90
+ const { chains } = (0, strategy_planner_1.selectCapabilityChains)("data", mockIR, 1);
91
+ (0, vitest_1.expect)(chains.length).toBeLessThanOrEqual(1);
92
+ });
93
+ (0, vitest_1.it)("skips planner internals (self-referential guard)", () => {
94
+ const selfIR = [{
95
+ name: "selectCapabilityChains",
96
+ purpose: "planner logic",
97
+ tags: [],
98
+ requires: [],
99
+ produces: [],
100
+ file: "src/strategy-planner.ts",
101
+ exported: true,
102
+ params: [],
103
+ returnType: "any",
104
+ }];
105
+ const { chains } = (0, strategy_planner_1.selectCapabilityChains)("planning", selfIR, 3);
106
+ // strategy-planner.ts and planner.ts should be filtered out
107
+ const names = chains.flatMap(c => c.nodes.map(n => n.name));
108
+ (0, vitest_1.expect)(names).not.toContain("selectCapabilityChains");
109
+ });
110
+ });
111
+ (0, vitest_1.describe)("selectCapabilityChains — edge cases", () => {
112
+ (0, vitest_1.it)("returns empty array for null intent", () => {
113
+ (0, vitest_1.expect)((0, strategy_planner_1.selectCapabilityChains)(null, mockIR)).toEqual({ chains: [], needsLLM: false });
114
+ });
115
+ (0, vitest_1.it)("returns empty array for undefined intent", () => {
116
+ (0, vitest_1.expect)((0, strategy_planner_1.selectCapabilityChains)(undefined, mockIR)).toEqual({ chains: [], needsLLM: false });
117
+ });
118
+ (0, vitest_1.it)("returns empty array for empty string intent", () => {
119
+ (0, vitest_1.expect)((0, strategy_planner_1.selectCapabilityChains)("", mockIR)).toEqual({ chains: [], needsLLM: false });
120
+ });
121
+ (0, vitest_1.it)("returns empty array for whitespace-only intent", () => {
122
+ (0, vitest_1.expect)((0, strategy_planner_1.selectCapabilityChains)(" ", mockIR)).toEqual({ chains: [], needsLLM: false });
123
+ });
124
+ });
125
+ (0, vitest_1.describe)("formatChainHint", () => {
126
+ (0, vitest_1.it)("returns empty string for empty chains", () => {
127
+ (0, vitest_1.expect)((0, strategy_planner_1.formatChainHint)([])).toBe("");
128
+ });
129
+ (0, vitest_1.it)("formats a single chain", () => {
130
+ const { chains } = (0, strategy_planner_1.selectCapabilityChains)("authenticate user", mockIR, 1);
131
+ const hint = (0, strategy_planner_1.formatChainHint)(chains);
132
+ (0, vitest_1.expect)(hint).toContain("建议的下一步调用");
133
+ (0, vitest_1.expect)(hint).toContain("★");
134
+ });
135
+ });