@hmharness/evolution 0.14.26 → 0.14.28

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.
@@ -9,6 +9,11 @@ export interface Insight {
9
9
  /** P0 impact attribution: skills (incl. canaries) injected into this
10
10
  * session's system prompt - the join key for canary A/B comparison. */
11
11
  skillsInjected?: string[];
12
+ /** Plan C (2026-09-27): the workspace (project) this session ran in.
13
+ * Retrieval stays GLOBAL - a lesson learned in project A is reachable
14
+ * from project B - but the origin is stamped on the injected line so
15
+ * the model can weigh "same tooling, different codebase" advice. */
16
+ workspace?: string;
12
17
  }
13
18
  export declare function redactSecrets(text: string): string;
14
19
  export declare function recordInsight(home: string, insight: Insight): Promise<void>;
@@ -16,3 +21,27 @@ export declare function recordInsight(home: string, insight: Insight): Promise<v
16
21
  export declare function readInsights(home: string, limit?: number): Promise<Insight[]>;
17
22
  /** Summarize recent insights for the system prompt (bounded). */
18
23
  export declare function recentInsights(home: string, limit?: number): Promise<string>;
24
+ /** Task-similarity feature set: per-WORD bigrams plus single chars as
25
+ * tokens (2026-09-27). Cross-word bigrams that include the space turned
26
+ * out to be a noise factory - 'fix' and 'xxx' shared 'x ' and unrelated
27
+ * natural sentences scored 0.06. Words are split on non-alphanumerics;
28
+ * CJK text has no spaces, so a whole Chinese clause becomes one word and
29
+ * keeps every character bigram. Pure. */
30
+ export declare function bigrams(s: string): Set<string>;
31
+ /** Jaccard similarity of two bigram sets. Pure. */
32
+ export declare function jaccard(a: Set<string>, b: Set<string>): number;
33
+ /** Noise floor: below this similarity an insight is unrelated to the task.
34
+ * With per-word bigrams the highest unrelated natural-language pairs score
35
+ * ~0.04 (stopword overlap: 'the' contributes th/he); related tasks land at
36
+ * 0.5+. The floor sits between the two, with margin on both sides. */
37
+ export declare const INSIGHT_SIM_FLOOR = 0.05;
38
+ /**
39
+ * The K most RELEVANT insights for this task (recency as tiebreak), falling
40
+ * back to the K most recent when nothing clears the noise floor - so the
41
+ * prompt always carries some experience, exactly like before.
42
+ */
43
+ export declare function retrieveInsights(home: string, task: string, opts?: {
44
+ topK?: number;
45
+ pool?: number;
46
+ workspace?: string;
47
+ }): Promise<string>;
package/dist/insights.js CHANGED
@@ -78,3 +78,92 @@ export async function recentInsights(home, limit = 5) {
78
78
  return '';
79
79
  }
80
80
  }
81
+ /* ---------------- experience retrieval (2026-09-27, plan A) ----------------
82
+ * The injection used to be "last 5 insights" - pure recency. After 300
83
+ * tasks, a question similar to task #50 got five UNRELATED recent lessons
84
+ * instead of that one. Industry direction (Agent KB / Agent Workflow
85
+ * Memory, verified 2026-09-27): retrieve relevant experience per task.
86
+ * Zero-dependency red line -> character-bigram Jaccard (works well for CJK
87
+ * too), a small recency tiebreak, graceful fallback to recency order. */
88
+ /** Task-similarity feature set: per-WORD bigrams plus single chars as
89
+ * tokens (2026-09-27). Cross-word bigrams that include the space turned
90
+ * out to be a noise factory - 'fix' and 'xxx' shared 'x ' and unrelated
91
+ * natural sentences scored 0.06. Words are split on non-alphanumerics;
92
+ * CJK text has no spaces, so a whole Chinese clause becomes one word and
93
+ * keeps every character bigram. Pure. */
94
+ export function bigrams(s) {
95
+ const words = s.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter(Boolean);
96
+ const out = new Set();
97
+ for (const w of words) {
98
+ if (w.length === 1) {
99
+ out.add(w);
100
+ continue;
101
+ }
102
+ for (let i = 0; i < w.length - 1; i++)
103
+ out.add(w.slice(i, i + 2));
104
+ }
105
+ return out;
106
+ }
107
+ /** Jaccard similarity of two bigram sets. Pure. */
108
+ export function jaccard(a, b) {
109
+ if (!a.size || !b.size)
110
+ return 0;
111
+ let inter = 0;
112
+ for (const g of a)
113
+ if (b.has(g))
114
+ inter++;
115
+ return inter / (a.size + b.size - inter);
116
+ }
117
+ function formatInsight(i, currentWorkspace) {
118
+ // cross-project origin stamp (plan C): lessons from ANOTHER project are
119
+ // still injected (global retrieval) but labelled, so the model knows the
120
+ // tooling lesson may not carry the codebase specifics with it
121
+ const from = i.workspace && currentWorkspace && i.workspace !== currentWorkspace ? ` [来自项目:${i.workspace}]` : '';
122
+ return `- [${i.outcome}] ${i.task.slice(0, 60)} (turns ${i.turns}, tools ${i.toolsUsed.join(',') || 'none'})${from}`;
123
+ }
124
+ /** Noise floor: below this similarity an insight is unrelated to the task.
125
+ * With per-word bigrams the highest unrelated natural-language pairs score
126
+ * ~0.04 (stopword overlap: 'the' contributes th/he); related tasks land at
127
+ * 0.5+. The floor sits between the two, with margin on both sides. */
128
+ export const INSIGHT_SIM_FLOOR = 0.05;
129
+ /**
130
+ * The K most RELEVANT insights for this task (recency as tiebreak), falling
131
+ * back to the K most recent when nothing clears the noise floor - so the
132
+ * prompt always carries some experience, exactly like before.
133
+ */
134
+ export async function retrieveInsights(home, task, opts = {}) {
135
+ const topK = opts.topK ?? 5;
136
+ const pool = opts.pool ?? 500;
137
+ let rows = [];
138
+ try {
139
+ const text = await readFile(join(home, 'insights', 'insights.jsonl'), 'utf8');
140
+ rows = text.trim().split('\n').filter(Boolean)
141
+ .map((l) => { try {
142
+ return JSON.parse(l);
143
+ }
144
+ catch {
145
+ return null;
146
+ } })
147
+ .filter((x) => x !== null)
148
+ .slice(-pool);
149
+ }
150
+ catch {
151
+ return ''; // no archive at all - same behaviour as recentInsights
152
+ }
153
+ if (rows.length === 0)
154
+ return '';
155
+ const q = bigrams(task);
156
+ if (q.size === 0)
157
+ return rows.slice(-topK).map((r) => formatInsight(r, opts.workspace)).join('\n');
158
+ // newest first within the pool, so idx IS the recency rank
159
+ const newestFirst = [...rows].reverse();
160
+ const scored = newestFirst
161
+ .map((r, idx) => ({ r, s: jaccard(q, bigrams(r.task)) + (1 - idx / (newestFirst.length + 1)) * 1e-4 }))
162
+ .filter((x) => x.s > INSIGHT_SIM_FLOOR)
163
+ .sort((a, b) => b.s - a.s)
164
+ .slice(0, topK)
165
+ .map((x) => x.r);
166
+ if (scored.length === 0)
167
+ return rows.slice(-topK).map((r) => formatInsight(r, opts.workspace)).join('\n');
168
+ return scored.map((r) => formatInsight(r, opts.workspace)).join('\n');
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/evolution",
3
- "version": "0.14.26",
3
+ "version": "0.14.28",
4
4
  "description": "hmharness evolution subsystem: persistent memory, insight capture, skill library, and the bench that gives evolution its fitness signal. First-class kernel citizen, not a plugin.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",