progmune-runtime 2.1.4 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,7 @@ exports.WorkMemory = void 0;
37
37
  exports.recordEpisode = recordEpisode;
38
38
  exports.getRecentEpisodes = getRecentEpisodes;
39
39
  exports.getSuccessfulEpisodes = getSuccessfulEpisodes;
40
+ exports.pruneEpisodicMemory = pruneEpisodicMemory;
40
41
  exports.consolidateSemantic = consolidateSemantic;
41
42
  exports.findSemanticTemplate = findSemanticTemplate;
42
43
  const fs = __importStar(require("fs"));
@@ -100,10 +101,14 @@ function recordEpisode(episode) {
100
101
  timestamp: new Date().toISOString(),
101
102
  };
102
103
  episodes.unshift(newEpisode);
103
- if (episodes.length > MAX_EPISODES) {
104
+ // Simple cap: trim to prevent runaway growth between GC cycles
105
+ if (episodes.length > MAX_EPISODES * 1.5) {
104
106
  episodes.length = MAX_EPISODES;
105
107
  }
106
108
  saveEpisodes(episodes);
109
+ // Run GC every 100 episodes
110
+ if (episodes.length % 100 === 0)
111
+ pruneEpisodicMemory();
107
112
  }
108
113
  /** @requires LIMIT @produces EPISODE_LIST */
109
114
  function getRecentEpisodes(limit = 10) {
@@ -112,6 +117,35 @@ function getRecentEpisodes(limit = 10) {
112
117
  function getSuccessfulEpisodes(limit = 10) {
113
118
  return loadEpisodes().filter(e => e.success).slice(0, limit);
114
119
  }
120
+ // ── Semantic GC: prune episodic memory ──
121
+ const MAX_EPISODE_COUNT = 1000;
122
+ const MAX_AGE_DAYS = 30;
123
+ /** Prune episodic memory: keep high-value, recent, diverse episodes.
124
+ * Removes: old failures (>30 days), low-value duplicates, excess beyond max.
125
+ * Called periodically after recording new episodes. */
126
+ function pruneEpisodicMemory() {
127
+ const episodes = loadEpisodes();
128
+ if (episodes.length <= MAX_EPISODE_COUNT)
129
+ return 0;
130
+ const now = Date.now();
131
+ const scored = episodes.map((ep, i) => {
132
+ const ageDays = (now - new Date(ep.timestamp).getTime()) / 86400000;
133
+ // Score: success=+2, recent=+3, older=-1/day
134
+ let score = ep.success ? 2 : 0;
135
+ score += Math.max(0, 3 - ageDays * 0.5); // recent bonus, decays over 6 days
136
+ score -= Math.max(0, (ageDays - MAX_AGE_DAYS) * 0.5); // penalty for >30 days
137
+ return { ep, score, index: i };
138
+ });
139
+ // Keep top MAX_EPISODES by score
140
+ scored.sort((a, b) => b.score - a.score);
141
+ const kept = scored.slice(0, MAX_EPISODE_COUNT).sort((a, b) => a.index - b.index);
142
+ const removed = episodes.length - kept.length;
143
+ if (removed > 0) {
144
+ saveEpisodes(kept.map(s => s.ep));
145
+ console.error(`[Memory] Semantic GC: removed ${removed} low-value episodes, kept ${kept.length}`);
146
+ }
147
+ return removed;
148
+ }
115
149
  const SEMANTIC_FILE = path.join(MEMORY_DIR, "semantic.json");
116
150
  const SEMANTIC_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 天
117
151
  function loadSemantic() {
@@ -174,18 +208,28 @@ function consolidateSemantic(minOccurrences = 3) {
174
208
  saveSemantic(templates);
175
209
  console.error(`[语义记忆] 巩固完成,模板数量: ${templates.length}`);
176
210
  }
177
- /** @requires INTENT @produces TEMPLATE */
211
+ /** @requires INTENT @produces TEMPLATE
212
+ * Uses keyword overlap (replaces prefix matching) for semantic recall. */
178
213
  function findSemanticTemplate(intent) {
179
214
  const templates = loadSemantic();
180
215
  if (templates.length === 0)
181
216
  return undefined;
182
- const prefix = intent.substring(0, 20).toLowerCase();
183
- const exactMatch = templates.find(t => t.intentPattern.toLowerCase() === prefix);
184
- if (exactMatch && exactMatch.successRate >= 0.7)
185
- return exactMatch;
186
- const fuzzyMatch = templates.find(t => {
187
- const pattern = t.intentPattern.toLowerCase();
188
- return (prefix.includes(pattern) || pattern.includes(prefix)) && t.successRate >= 0.8;
189
- });
190
- return fuzzyMatch;
217
+ // Extract keywords from intent
218
+ const words = new Set(intent.toLowerCase().split(/[\s,,]+/).filter(w => w.length > 2));
219
+ if (words.size === 0)
220
+ return undefined;
221
+ // Score templates by keyword overlap
222
+ let best;
223
+ let bestScore = 0;
224
+ for (const t of templates) {
225
+ const tWords = t.intentPattern.toLowerCase().split(/[\s,,]+/);
226
+ const shared = tWords.filter(w => words.has(w)).length;
227
+ const total = new Set([...words, ...new Set(tWords)]).size;
228
+ const score = shared / (total || 1); // Jaccard-like
229
+ if (score > 0.5 && t.successRate >= 0.7 && score > bestScore) {
230
+ bestScore = score;
231
+ best = t;
232
+ }
233
+ }
234
+ return best;
191
235
  }
package/dist/planner.js CHANGED
@@ -750,6 +750,17 @@ async function plan(userIntent) {
750
750
  score += matchCount * 0.2;
751
751
  }
752
752
  }
753
+ // Semantic Capability: useWhen scenario matching
754
+ if (f.useWhen) {
755
+ for (const scenario of f.useWhen) {
756
+ const scenarioWords = scenario.toLowerCase().split(/[\s,]+/);
757
+ const matchCount = scenarioWords.filter((w) => w.length > 3 && intentLower.includes(w)).length;
758
+ if (matchCount >= 2)
759
+ score += 3.0; // strong signal: intent matches use case
760
+ else if (matchCount === 1)
761
+ score += 1.0;
762
+ }
763
+ }
753
764
  // Capability Graph: tag match
754
765
  if (f.tags) {
755
766
  for (const tag of f.tags) {
@@ -758,7 +769,7 @@ async function plan(userIntent) {
758
769
  }
759
770
  }
760
771
  // Dynamic Credit: multiply by actual success rate (0.1-1.0)
761
- const successRate = (0, feedback_1.getFunctionSuccessRate)(f.name);
772
+ const successRate = (0, feedback_1.getFailureAdjustedCredit)(f.name);
762
773
  const creditFactor = 0.3 + successRate * 0.7; // range: 0.3 (always fail) to 1.0 (always succeed)
763
774
  if (f.exported && !f.external)
764
775
  score *= creditFactor;
@@ -6,6 +6,7 @@ exports.formatSessionCounts = formatSessionCounts;
6
6
  * @requires SESSION_LIST @produces RESOLVED_COUNT
7
7
  * @tags session, count, statistics
8
8
  */
9
+ /** @useWhen checking how many sessions succeeded; computing success rate */
9
10
  function countResolved(sessions) {
10
11
  const resolved = sessions.filter((s) => s.resolved).length;
11
12
  return { resolved, unresolved: sessions.length - resolved, total: sessions.length };
package/dist/stdlib.js CHANGED
@@ -32,10 +32,12 @@ exports.retry = retry;
32
32
  exports.debounce = debounce;
33
33
  // ── String ──
34
34
  /** @requires STRING @produces VALIDATION_RESULT @tags string, email, validation */
35
+ /** @useWhen validating user input; form validation; email checking */
35
36
  function isValidEmail(str) {
36
37
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
37
38
  }
38
39
  /** @requires STRING @produces TRUNCATED_STRING @tags string, format */
40
+ /** @useWhen displaying preview text; limiting UI output; shortening strings */
39
41
  function truncate(str, maxLen, ellipsis = "...") {
40
42
  return str.length <= maxLen ? str : str.slice(0, maxLen - ellipsis.length) + ellipsis;
41
43
  }
@@ -88,6 +90,7 @@ function arrayDiff(a, b) {
88
90
  }
89
91
  // ── Math ──
90
92
  /** @requires NUMBERS @produces AVERAGE @tags math, statistics */
93
+ /** @useWhen computing statistics; data analysis; metrics calculation */
91
94
  function average(nums) {
92
95
  return nums.length === 0 ? 0 : nums.reduce((a, b) => a + b, 0) / nums.length;
93
96
  }
@@ -12,6 +12,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.selectCapabilityChains = selectCapabilityChains;
13
13
  exports.formatChainHint = formatChainHint;
14
14
  const utils_1 = require("./utils");
15
+ const feedback_1 = require("./feedback");
16
+ const semantic_topology_1 = require("./semantic-topology");
15
17
  /** Build a capability graph from IR functions. */
16
18
  function buildCapabilityGraph(ir) {
17
19
  const graph = new Map();
@@ -24,57 +26,129 @@ function buildCapabilityGraph(ir) {
24
26
  tags: f.tags || [],
25
27
  requires: f.requires || [],
26
28
  produces: f.produces || [],
29
+ useWhen: f.useWhen || [],
27
30
  score: 0,
28
31
  });
29
32
  }
30
33
  return graph;
31
34
  }
32
- /** Score a capability node against an intent. */
35
+ /** Score a capability node against an intent.
36
+ * Returns 0 for irrelevant nodes (no keyword match at all). */
33
37
  function scoreNode(node, intentLower, keywords) {
34
38
  let score = 0;
39
+ let hasMatch = false;
35
40
  // Name match
36
41
  for (const kw of keywords) {
37
- if (node.name.toLowerCase().includes(kw))
42
+ if (node.name.toLowerCase().includes(kw)) {
38
43
  score += 1;
39
- score += (0, utils_1.jaccardSimilarity)(node.name.toLowerCase(), kw);
44
+ hasMatch = true;
45
+ }
46
+ const js = (0, utils_1.jaccardSimilarity)(node.name.toLowerCase(), kw);
47
+ if (js > 0.2) {
48
+ score += js;
49
+ hasMatch = true;
50
+ }
40
51
  }
41
52
  // Purpose match
42
53
  const purposeLower = node.purpose.toLowerCase();
43
54
  for (const kw of keywords) {
44
- if (purposeLower.includes(kw))
55
+ if (purposeLower.includes(kw)) {
45
56
  score += 2;
57
+ hasMatch = true;
58
+ }
46
59
  }
47
60
  // Tag match
48
61
  for (const tag of node.tags) {
49
- if (intentLower.includes(tag.toLowerCase()))
62
+ if (intentLower.includes(tag.toLowerCase())) {
50
63
  score += 1.5;
64
+ hasMatch = true;
65
+ }
51
66
  }
52
67
  // Semantic word overlap in purpose
53
68
  const intentWords = intentLower.split(/[\s,,]+/);
54
69
  for (const w of intentWords) {
55
- if (w.length > 2 && purposeLower.includes(w))
70
+ if (w.length > 2 && purposeLower.includes(w)) {
56
71
  score += 0.5;
72
+ hasMatch = true;
73
+ }
57
74
  }
58
- return score;
75
+ // useWhen scenario match
76
+ if (node.useWhen) {
77
+ for (const scenario of node.useWhen) {
78
+ const scenarioWords = scenario.toLowerCase().split(/[\s,]+/);
79
+ const matchCount = scenarioWords.filter((w) => w.length > 3 && intentLower.includes(w)).length;
80
+ if (matchCount >= 2) {
81
+ score += 3.0;
82
+ hasMatch = true;
83
+ }
84
+ else if (matchCount === 1) {
85
+ score += 1.0;
86
+ hasMatch = true;
87
+ }
88
+ }
89
+ }
90
+ // Require at least one match to be relevant
91
+ if (!hasMatch)
92
+ return 0;
93
+ // Dynamic Credit: multiply by actual success rate
94
+ const successRate = (0, feedback_1.getFailureAdjustedCredit)(node.name);
95
+ const creditFactor = 0.3 + successRate * 0.7;
96
+ return score * creditFactor;
59
97
  }
60
- /** Find all capability nodes that produce a given capability label. */
61
- function findProducers(graph, capability) {
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) {
62
101
  const producers = [];
63
102
  for (const node of graph.values()) {
64
103
  if (node.produces.some(p => p === capability || capability.includes(p) || p.includes(capability))) {
65
104
  producers.push(node);
66
105
  }
67
106
  }
107
+ // Topology fallback: find semantically related producers
108
+ if (producers.length === 0) {
109
+ try {
110
+ const topo = (0, semantic_topology_1.getTopology)();
111
+ for (const node of allNodes) {
112
+ if (node.produces.length > 0) {
113
+ for (const p of node.produces) {
114
+ if (topo.capabilityMatch(p, capability) && !producers.includes(node)) {
115
+ producers.push(node);
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ catch { }
123
+ }
68
124
  return producers;
69
125
  }
70
- /** Find all capability nodes that require a given capability label. */
71
- function findConsumers(graph, capability) {
126
+ /** Find all capability nodes that require a given capability label.
127
+ * Falls back to topology similarity if no direct data-flow match. */
128
+ function findConsumers(graph, capability, allNodes) {
72
129
  const consumers = [];
73
130
  for (const node of graph.values()) {
74
131
  if (node.requires.some(r => r === capability || capability.includes(r) || r.includes(capability))) {
75
132
  consumers.push(node);
76
133
  }
77
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
+ }
78
152
  return consumers;
79
153
  }
80
154
  /**
@@ -94,39 +168,70 @@ function selectCapabilityChains(intent, ir, maxChains = 5) {
94
168
  for (const node of graph.values()) {
95
169
  node.score = scoreNode(node, intentLower, keywords);
96
170
  }
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);
171
+ // Dynamic threshold: tighten for large IR to prevent score dilution
172
+ let dynamicThreshold = graph.size > 500 ? 2.0 : graph.size > 200 ? 1.5 : 1.0;
173
+ // Fallback: if no seeds found, halve threshold
174
+ let seeds = [...graph.values()]
175
+ .filter(n => n.score > dynamicThreshold && (n.produces.length > 0 || n.score > dynamicThreshold + 2))
176
+ .sort((a, b) => b.score - a.score);
177
+ if (seeds.length === 0 && dynamicThreshold > 0.5) {
178
+ dynamicThreshold *= 0.5;
179
+ seeds = [...graph.values()]
180
+ .filter(n => n.score > dynamicThreshold && (n.produces.length > 0 || n.score > dynamicThreshold + 1))
181
+ .sort((a, b) => b.score - a.score);
182
+ }
183
+ seeds = seeds.slice(0, graph.size > 500 ? 30 : 15);
184
+ const allNodes = [...graph.values()];
102
185
  const chains = [];
103
186
  for (const seed of seeds) {
104
187
  // Build chain: seed → consumer → consumer...
105
188
  const chain = [seed];
106
189
  const visited = new Set([seed.name]);
107
190
  let totalScore = seed.score;
108
- // Forward trace: for each produce of the last node, find consumers
191
+ // Forward trace: data flow semantic leap
109
192
  let current = seed;
110
193
  let extended = true;
194
+ let leapDecay = 1.0; // weight decay for semantic leaps
111
195
  while (extended && chain.length < 8) {
112
196
  extended = false;
197
+ // Strategy 1: direct data flow (produces → requires)
113
198
  for (const p of current.produces) {
114
- const consumers = findConsumers(graph, p).filter(c => !visited.has(c.name));
199
+ const consumers = findConsumers(graph, p, allNodes).filter(c => !visited.has(c.name));
115
200
  if (consumers.length > 0) {
116
- // Pick best-scoring consumer
117
201
  const bestConsumer = consumers.sort((a, b) => b.score - a.score)[0];
118
202
  chain.push(bestConsumer);
119
203
  visited.add(bestConsumer.name);
120
204
  totalScore += bestConsumer.score;
121
205
  current = bestConsumer;
122
206
  extended = true;
207
+ leapDecay = 1.0; // reset decay on direct match
123
208
  break;
124
209
  }
125
210
  }
211
+ // Strategy 2: semantic leap — use topology similarity
212
+ if (!extended) {
213
+ try {
214
+ const topo = (0, semantic_topology_1.getTopology)();
215
+ const similar = topo.findSimilar(current.name, 10)
216
+ .filter(s => !visited.has(s.name) && s.similarity > 0.2);
217
+ if (similar.length > 0) {
218
+ const bestMatch = graph.get(similar[0].name);
219
+ if (bestMatch && bestMatch.score > 0) {
220
+ chain.push(bestMatch);
221
+ visited.add(bestMatch.name);
222
+ totalScore += bestMatch.score * leapDecay; // decayed score
223
+ current = bestMatch;
224
+ extended = true;
225
+ leapDecay *= 0.7; // each semantic leap loses 30% weight
226
+ }
227
+ }
228
+ }
229
+ catch { }
230
+ }
126
231
  }
127
232
  // Backward trace: does seed need something? Find producers.
128
233
  if (seed.requires.length > 0) {
129
- const producers = findProducers(graph, seed.requires[0])
234
+ const producers = findProducers(graph, seed.requires[0], allNodes)
130
235
  .filter(p => !visited.has(p.name));
131
236
  if (producers.length > 0) {
132
237
  const bestProducer = producers.sort((a, b) => b.score - a.score)[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "Progmune Runtime — Program Immunology: Constraint-Guided Program Synthesis Runtime",
5
5
  "main": "dist/mcp-server.mjs",
6
6
  "bin": {