progmune-runtime 2.1.3 → 2.1.4
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/planner.js +32 -6
- package/dist/semantic-topology.js +160 -0
- package/package.json +1 -1
package/dist/planner.js
CHANGED
|
@@ -39,6 +39,7 @@ const runtime_types_1 = require("./runtime-types");
|
|
|
39
39
|
const action_runtime_1 = require("./action-runtime");
|
|
40
40
|
const validator_1 = require("./validator");
|
|
41
41
|
const semantic_validator_1 = require("./semantic-validator");
|
|
42
|
+
const feedback_1 = require("./feedback");
|
|
42
43
|
const utils_1 = require("./utils");
|
|
43
44
|
const failure_corpus_1 = require("./failure-corpus");
|
|
44
45
|
const memory_layer_1 = require("./memory-layer");
|
|
@@ -46,6 +47,7 @@ const ssg_validator_1 = require("./ssg-validator");
|
|
|
46
47
|
const protocol_registry_1 = require("./protocol-registry");
|
|
47
48
|
const semantic_snapshot_1 = require("./semantic-snapshot");
|
|
48
49
|
const strategy_planner_1 = require("./strategy-planner");
|
|
50
|
+
const semantic_topology_1 = require("./semantic-topology");
|
|
49
51
|
const fs = __importStar(require("fs"));
|
|
50
52
|
function enrichActions(actions, ir) {
|
|
51
53
|
return actions.map(a => {
|
|
@@ -119,17 +121,20 @@ function buildCompactFuncList(funcs, allFuncs) {
|
|
|
119
121
|
}).join("\n");
|
|
120
122
|
}
|
|
121
123
|
/** Semantic matching: check if two capability labels are related.
|
|
122
|
-
* Uses
|
|
124
|
+
* Uses SemanticTopology (structural graph) instead of string matching. */
|
|
123
125
|
function semanticMatch(a, b) {
|
|
126
|
+
try {
|
|
127
|
+
const topo = (0, semantic_topology_1.getTopology)();
|
|
128
|
+
if (topo.size > 0)
|
|
129
|
+
return topo.capabilityMatch(a, b);
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
// Fallback: exact + substring
|
|
124
133
|
if (a === b)
|
|
125
134
|
return true;
|
|
126
135
|
if (a.includes(b) || b.includes(a))
|
|
127
136
|
return true;
|
|
128
|
-
|
|
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;
|
|
137
|
+
return false;
|
|
133
138
|
}
|
|
134
139
|
/** Build capability chain hints from IR: producer→consumer relationships.
|
|
135
140
|
* Uses semantic matching for fuzzy capability linking. */
|
|
@@ -551,6 +556,11 @@ async function plan(userIntent) {
|
|
|
551
556
|
const irRaw = JSON.parse(fs.readFileSync("ir.json", "utf-8"));
|
|
552
557
|
// Support both old (array) and new ({typeMap, functions}) formats
|
|
553
558
|
const ir = Array.isArray(irRaw) ? irRaw : (irRaw.functions || []);
|
|
559
|
+
// P1: Build Semantic Topology (once per plan call, cached)
|
|
560
|
+
try {
|
|
561
|
+
(0, semantic_topology_1.rebuildTopology)(ir);
|
|
562
|
+
}
|
|
563
|
+
catch { }
|
|
554
564
|
// Helper: wrap actions into PlanResult
|
|
555
565
|
let repairMetrics = { applied: false, count: 0, branchIds: [] };
|
|
556
566
|
const wrapResult = (actions, repair) => ({
|
|
@@ -747,6 +757,11 @@ async function plan(userIntent) {
|
|
|
747
757
|
score += 0.8;
|
|
748
758
|
}
|
|
749
759
|
}
|
|
760
|
+
// Dynamic Credit: multiply by actual success rate (0.1-1.0)
|
|
761
|
+
const successRate = (0, feedback_1.getFunctionSuccessRate)(f.name);
|
|
762
|
+
const creditFactor = 0.3 + successRate * 0.7; // range: 0.3 (always fail) to 1.0 (always succeed)
|
|
763
|
+
if (f.exported && !f.external)
|
|
764
|
+
score *= creditFactor;
|
|
750
765
|
return { ...f, score };
|
|
751
766
|
});
|
|
752
767
|
scored.sort((a, b) => b.score - a.score);
|
|
@@ -956,6 +971,17 @@ ${RETRY_HINT}
|
|
|
956
971
|
}
|
|
957
972
|
}
|
|
958
973
|
}
|
|
974
|
+
// P0: Strategy Enforcement — LLM must follow recommended chain
|
|
975
|
+
if (chains.length > 0 && chains[0].nodes.length >= 2) {
|
|
976
|
+
const topChain = chains[0];
|
|
977
|
+
const requiredFuncs = topChain.nodes.map(n => n.name);
|
|
978
|
+
const chosenFuncs = filtered.filter(a => a.kind === "call").map(a => a.function);
|
|
979
|
+
const missing = requiredFuncs.filter(fn => !chosenFuncs.includes(fn));
|
|
980
|
+
if (missing.length >= requiredFuncs.length * 0.5) {
|
|
981
|
+
// More than 50% of the chain is missing — LLM ignored the strategy
|
|
982
|
+
preCheckErrors.push(`策略违规: 推荐链 ${topChain.explanation},但缺少 ${missing.join(", ")}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
959
985
|
// 1) 基础序列校验
|
|
960
986
|
const seqResult = (0, validator_1.validateActionSequence)(filtered);
|
|
961
987
|
if (!seqResult.valid || preCheckErrors.length > 0) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 8: Semantic Topology (P1)
|
|
4
|
+
*
|
|
5
|
+
* Builds a similarity graph from IR structural data:
|
|
6
|
+
* - File co-occurrence (functions in same file are related)
|
|
7
|
+
* - Tag overlap (shared domain tags)
|
|
8
|
+
* - Purpose word overlap (Jaccard on purpose text)
|
|
9
|
+
* - Chain adjacency (producer→consumer links)
|
|
10
|
+
*
|
|
11
|
+
* Replaces simple string matching in capability search.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SemanticTopology = void 0;
|
|
15
|
+
exports.getTopology = getTopology;
|
|
16
|
+
exports.rebuildTopology = rebuildTopology;
|
|
17
|
+
class SemanticTopology {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.nodes = new Map();
|
|
20
|
+
this.edges = new Map();
|
|
21
|
+
this.similarityCache = new Map();
|
|
22
|
+
}
|
|
23
|
+
/** Build topology from IR data */
|
|
24
|
+
build(ir) {
|
|
25
|
+
this.nodes.clear();
|
|
26
|
+
this.edges.clear();
|
|
27
|
+
this.similarityCache.clear();
|
|
28
|
+
// 1. Create nodes
|
|
29
|
+
for (const f of ir) {
|
|
30
|
+
if (!f.exported && !f.external)
|
|
31
|
+
continue;
|
|
32
|
+
this.nodes.set(f.name, {
|
|
33
|
+
name: f.name,
|
|
34
|
+
file: f.file || "",
|
|
35
|
+
tags: new Set((f.tags || []).map((t) => t.toLowerCase())),
|
|
36
|
+
purposeWords: new Set((f.purpose || "").toLowerCase().split(/[\s,,]+/).filter((w) => w.length > 2)),
|
|
37
|
+
produces: new Set(f.produces || []),
|
|
38
|
+
requires: new Set(f.requires || []),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// 2. Build edges: file co-occurrence
|
|
42
|
+
const byFile = new Map();
|
|
43
|
+
for (const [name, node] of this.nodes) {
|
|
44
|
+
if (!byFile.has(node.file))
|
|
45
|
+
byFile.set(node.file, []);
|
|
46
|
+
byFile.get(node.file).push(name);
|
|
47
|
+
}
|
|
48
|
+
for (const names of byFile.values()) {
|
|
49
|
+
for (let i = 0; i < names.length; i++) {
|
|
50
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
51
|
+
this.addEdge(names[i], names[j], 0.3, "co-file");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 3. Build edges: tag overlap
|
|
56
|
+
for (const [nameA, nodeA] of this.nodes) {
|
|
57
|
+
for (const [nameB, nodeB] of this.nodes) {
|
|
58
|
+
if (nameA >= nameB)
|
|
59
|
+
continue;
|
|
60
|
+
const tagOverlap = [...nodeA.tags].filter(t => nodeB.tags.has(t)).length;
|
|
61
|
+
if (tagOverlap > 0) {
|
|
62
|
+
const maxTags = Math.max(nodeA.tags.size, nodeB.tags.size) || 1;
|
|
63
|
+
this.addEdge(nameA, nameB, 0.4 * (tagOverlap / maxTags), "tag");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 4. Build edges: purpose word overlap
|
|
68
|
+
for (const [nameA, nodeA] of this.nodes) {
|
|
69
|
+
for (const [nameB, nodeB] of this.nodes) {
|
|
70
|
+
if (nameA >= nameB)
|
|
71
|
+
continue;
|
|
72
|
+
const shared = [...nodeA.purposeWords].filter(w => nodeB.purposeWords.has(w)).length;
|
|
73
|
+
const total = [...new Set([...nodeA.purposeWords, ...nodeB.purposeWords])].length || 1;
|
|
74
|
+
const jaccard = shared / total;
|
|
75
|
+
if (jaccard > 0.15) {
|
|
76
|
+
this.addEdge(nameA, nameB, 0.5 * jaccard, "purpose");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 5. Build edges: chain adjacency (producer→consumer)
|
|
81
|
+
for (const [nameA, nodeA] of this.nodes) {
|
|
82
|
+
for (const p of nodeA.produces) {
|
|
83
|
+
for (const [nameB, nodeB] of this.nodes) {
|
|
84
|
+
if (nameA === nameB)
|
|
85
|
+
continue;
|
|
86
|
+
if (nodeB.requires.has(p)) {
|
|
87
|
+
this.addEdge(nameA, nameB, 0.7, `chain:${p}`);
|
|
88
|
+
}
|
|
89
|
+
// Fuzzy chain: substring match
|
|
90
|
+
for (const r of nodeB.requires) {
|
|
91
|
+
if (p.includes(r) || r.includes(p)) {
|
|
92
|
+
this.addEdge(nameA, nameB, 0.4, `fuzzy:${p}≈${r}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
addEdge(a, b, weight, reason) {
|
|
100
|
+
const key = a < b ? `${a}::${b}` : `${b}::${a}`;
|
|
101
|
+
if (!this.edges.has(a))
|
|
102
|
+
this.edges.set(a, []);
|
|
103
|
+
if (!this.edges.has(b))
|
|
104
|
+
this.edges.set(b, []);
|
|
105
|
+
this.edges.get(a).push({ source: a, target: b, weight, reason });
|
|
106
|
+
this.edges.get(b).push({ source: b, target: a, weight, reason });
|
|
107
|
+
this.similarityCache.set(key, Math.max(this.similarityCache.get(key) || 0, weight));
|
|
108
|
+
}
|
|
109
|
+
/** Get similarity between two functions (0-1). */
|
|
110
|
+
similarity(funcA, funcB) {
|
|
111
|
+
if (funcA === funcB)
|
|
112
|
+
return 1.0;
|
|
113
|
+
const key = funcA < funcB ? `${funcA}::${funcB}` : `${funcB}::${funcA}`;
|
|
114
|
+
return this.similarityCache.get(key) || 0;
|
|
115
|
+
}
|
|
116
|
+
/** Find top N most similar functions to a given function. */
|
|
117
|
+
findSimilar(funcName, topN = 5) {
|
|
118
|
+
const edges = this.edges.get(funcName) || [];
|
|
119
|
+
return edges
|
|
120
|
+
.sort((a, b) => b.weight - a.weight)
|
|
121
|
+
.slice(0, topN)
|
|
122
|
+
.map(e => ({ name: e.target, similarity: e.weight }));
|
|
123
|
+
}
|
|
124
|
+
/** Semantic match: two capability labels are related via topology. */
|
|
125
|
+
capabilityMatch(produce, require) {
|
|
126
|
+
// Direct match
|
|
127
|
+
if (produce === require)
|
|
128
|
+
return true;
|
|
129
|
+
if (produce.includes(require) || require.includes(produce))
|
|
130
|
+
return true;
|
|
131
|
+
// Topology check: are there functions producing 'produce' that are connected
|
|
132
|
+
// to functions requiring 'require'?
|
|
133
|
+
const producers = [...this.nodes.values()].filter(n => n.produces.has(produce));
|
|
134
|
+
const consumers = [...this.nodes.values()].filter(n => n.requires.has(require));
|
|
135
|
+
for (const p of producers) {
|
|
136
|
+
for (const c of consumers) {
|
|
137
|
+
if (this.similarity(p.name, c.name) > 0.2)
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
/** Get node count */
|
|
144
|
+
get size() { return this.nodes.size; }
|
|
145
|
+
}
|
|
146
|
+
exports.SemanticTopology = SemanticTopology;
|
|
147
|
+
// Singleton
|
|
148
|
+
let _topology = null;
|
|
149
|
+
function getTopology(ir) {
|
|
150
|
+
if (!_topology && ir) {
|
|
151
|
+
_topology = new SemanticTopology();
|
|
152
|
+
_topology.build(ir);
|
|
153
|
+
}
|
|
154
|
+
return _topology || new SemanticTopology();
|
|
155
|
+
}
|
|
156
|
+
function rebuildTopology(ir) {
|
|
157
|
+
_topology = new SemanticTopology();
|
|
158
|
+
_topology.build(ir);
|
|
159
|
+
return _topology;
|
|
160
|
+
}
|