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.
- package/README.md +453 -79
- package/WHITEPAPER.md +213 -91
- package/dist/extract-ir.js +6 -0
- package/dist/failure-collector.js +2 -0
- package/dist/failure-corpus.js +4 -0
- package/dist/feedback.js +56 -2
- package/dist/health-utils.js +1 -0
- package/dist/ledger-registry.js +2 -0
- package/dist/memory-layer.js +55 -11
- package/dist/planner.js +12 -1
- package/dist/session-utils.js +1 -0
- package/dist/stdlib.js +3 -0
- package/dist/strategy-planner.js +125 -20
- package/package.json +1 -1
package/dist/memory-layer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.
|
|
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;
|
package/dist/session-utils.js
CHANGED
|
@@ -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
|
}
|
package/dist/strategy-planner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
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:
|
|
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];
|