@hmharness/evolution 0.14.19 → 0.14.21
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/context-evolution.d.ts +97 -0
- package/dist/context-evolution.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/router-evolution.d.ts +60 -0
- package/dist/router-evolution.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/evolution - Context Ranker Evolution (P2-04)
|
|
3
|
+
*
|
|
4
|
+
* The audit called for: "让 context policy 进入 Evolution"
|
|
5
|
+
*
|
|
6
|
+
* Makes the context ranker's weights evolvable: they can be registered
|
|
7
|
+
* as candidates, A/B tested, and promoted/rolled back via the same
|
|
8
|
+
* evolution machinery that handles skills and routing policies.
|
|
9
|
+
*
|
|
10
|
+
* The existing ContextRanker uses fixed RANK_WEIGHTS; this module wraps
|
|
11
|
+
* those weights in an experimentable policy.
|
|
12
|
+
*/
|
|
13
|
+
export interface ContextWeights {
|
|
14
|
+
/** relevance to current task */
|
|
15
|
+
relevance: number;
|
|
16
|
+
/** recency (newer = higher) */
|
|
17
|
+
recency: number;
|
|
18
|
+
/** frequency of access */
|
|
19
|
+
frequency: number;
|
|
20
|
+
/** token cost penalty (lower cost = higher rank) */
|
|
21
|
+
costEfficiency: number;
|
|
22
|
+
/** source trust (insights > generic) */
|
|
23
|
+
source: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const DEFAULT_CONTEXT_WEIGHTS: ContextWeights;
|
|
26
|
+
export interface ContextPolicy {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
weights: ContextWeights;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ContextEvolutionExperiment {
|
|
33
|
+
id: string;
|
|
34
|
+
challenger: ContextPolicy;
|
|
35
|
+
incumbent: ContextPolicy;
|
|
36
|
+
/** A/B results */
|
|
37
|
+
controlScores: number[];
|
|
38
|
+
treatmentScores: number[];
|
|
39
|
+
status: 'running' | 'promoted' | 'rolled-back' | 'inconclusive';
|
|
40
|
+
startedAt: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate that context weights are reasonable (sum to ~1, all non-negative).
|
|
44
|
+
* Pure - testable.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateWeights(w: ContextWeights): {
|
|
47
|
+
valid: boolean;
|
|
48
|
+
errors: string[];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Score a context item given weights and features.
|
|
52
|
+
* Pure - testable.
|
|
53
|
+
*/
|
|
54
|
+
export declare function scoreContextItem(weights: ContextWeights, features: {
|
|
55
|
+
relevance: number;
|
|
56
|
+
recency: number;
|
|
57
|
+
frequency: number;
|
|
58
|
+
tokenCost: number;
|
|
59
|
+
sourceTrust: number;
|
|
60
|
+
}): number;
|
|
61
|
+
/**
|
|
62
|
+
* Rank context items by score (highest first).
|
|
63
|
+
* Pure - testable.
|
|
64
|
+
*/
|
|
65
|
+
export declare function rankItems(weights: ContextWeights, items: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
features: Parameters<typeof scoreContextItem>[1];
|
|
68
|
+
}>): Array<{
|
|
69
|
+
id: string;
|
|
70
|
+
score: number;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Evaluate a context evolution experiment.
|
|
74
|
+
* Simple paired comparison of mean scores.
|
|
75
|
+
* Pure - testable.
|
|
76
|
+
*/
|
|
77
|
+
export declare function evaluateContextExperiment(exp: ContextEvolutionExperiment): {
|
|
78
|
+
decision: 'continue' | 'promote' | 'rollback';
|
|
79
|
+
reason: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Registry for context policies (similar to RoutingPolicyRegistry).
|
|
83
|
+
*/
|
|
84
|
+
export declare class ContextPolicyRegistry {
|
|
85
|
+
private policies;
|
|
86
|
+
private activeId;
|
|
87
|
+
register(policy: ContextPolicy): {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
reason?: string;
|
|
90
|
+
};
|
|
91
|
+
get(id: string): ContextPolicy | undefined;
|
|
92
|
+
list(): ContextPolicy[];
|
|
93
|
+
getActive(): ContextPolicy | undefined;
|
|
94
|
+
get activePolicyId(): string;
|
|
95
|
+
promote(id: string): boolean;
|
|
96
|
+
rollback(): void;
|
|
97
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/evolution - Context Ranker Evolution (P2-04)
|
|
3
|
+
*
|
|
4
|
+
* The audit called for: "让 context policy 进入 Evolution"
|
|
5
|
+
*
|
|
6
|
+
* Makes the context ranker's weights evolvable: they can be registered
|
|
7
|
+
* as candidates, A/B tested, and promoted/rolled back via the same
|
|
8
|
+
* evolution machinery that handles skills and routing policies.
|
|
9
|
+
*
|
|
10
|
+
* The existing ContextRanker uses fixed RANK_WEIGHTS; this module wraps
|
|
11
|
+
* those weights in an experimentable policy.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_CONTEXT_WEIGHTS = {
|
|
14
|
+
relevance: 0.40,
|
|
15
|
+
recency: 0.20,
|
|
16
|
+
frequency: 0.15,
|
|
17
|
+
costEfficiency: 0.10,
|
|
18
|
+
source: 0.15,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Validate that context weights are reasonable (sum to ~1, all non-negative).
|
|
22
|
+
* Pure - testable.
|
|
23
|
+
*/
|
|
24
|
+
export function validateWeights(w) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
const entries = Object.entries(w);
|
|
27
|
+
for (const [k, v] of entries) {
|
|
28
|
+
if (v < 0)
|
|
29
|
+
errors.push(`${k} must be >= 0, got ${v}`);
|
|
30
|
+
if (v > 1)
|
|
31
|
+
errors.push(`${k} must be <= 1, got ${v}`);
|
|
32
|
+
}
|
|
33
|
+
const sum = entries.reduce((a, [, v]) => a + v, 0);
|
|
34
|
+
if (Math.abs(sum - 1) > 0.05)
|
|
35
|
+
errors.push(`weights sum to ${sum.toFixed(2)}, expected ~1.0`);
|
|
36
|
+
return { valid: errors.length === 0, errors };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Score a context item given weights and features.
|
|
40
|
+
* Pure - testable.
|
|
41
|
+
*/
|
|
42
|
+
export function scoreContextItem(weights, features) {
|
|
43
|
+
const costEff = features.tokenCost > 0 ? 1 / (1 + features.tokenCost / 1000) : 1;
|
|
44
|
+
return (weights.relevance * features.relevance +
|
|
45
|
+
weights.recency * features.recency +
|
|
46
|
+
weights.frequency * features.frequency +
|
|
47
|
+
weights.costEfficiency * costEff +
|
|
48
|
+
weights.source * features.sourceTrust);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Rank context items by score (highest first).
|
|
52
|
+
* Pure - testable.
|
|
53
|
+
*/
|
|
54
|
+
export function rankItems(weights, items) {
|
|
55
|
+
return items
|
|
56
|
+
.map(item => ({ id: item.id, score: scoreContextItem(weights, item.features) }))
|
|
57
|
+
.sort((a, b) => b.score - a.score);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate a context evolution experiment.
|
|
61
|
+
* Simple paired comparison of mean scores.
|
|
62
|
+
* Pure - testable.
|
|
63
|
+
*/
|
|
64
|
+
export function evaluateContextExperiment(exp) {
|
|
65
|
+
const minN = 10;
|
|
66
|
+
if (exp.controlScores.length < minN || exp.treatmentScores.length < minN) {
|
|
67
|
+
return { decision: 'continue', reason: `insufficient samples (control ${exp.controlScores.length}/${minN}, treatment ${exp.treatmentScores.length}/${minN})` };
|
|
68
|
+
}
|
|
69
|
+
const ctrlMean = exp.controlScores.reduce((a, b) => a + b, 0) / exp.controlScores.length;
|
|
70
|
+
const trtMean = exp.treatmentScores.reduce((a, b) => a + b, 0) / exp.treatmentScores.length;
|
|
71
|
+
const diff = trtMean - ctrlMean;
|
|
72
|
+
// promote if challenger is meaningfully better
|
|
73
|
+
if (diff > 0.02)
|
|
74
|
+
return { decision: 'promote', reason: `treatment ${trtMean.toFixed(3)} vs control ${ctrlMean.toFixed(3)} (+${diff.toFixed(3)})` };
|
|
75
|
+
if (diff < -0.02)
|
|
76
|
+
return { decision: 'rollback', reason: `treatment ${trtMean.toFixed(3)} vs control ${ctrlMean.toFixed(3)} (${diff.toFixed(3)})` };
|
|
77
|
+
return { decision: 'continue', reason: `diff ${diff.toFixed(3)} within noise` };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Registry for context policies (similar to RoutingPolicyRegistry).
|
|
81
|
+
*/
|
|
82
|
+
export class ContextPolicyRegistry {
|
|
83
|
+
policies = new Map();
|
|
84
|
+
activeId = 'default';
|
|
85
|
+
register(policy) {
|
|
86
|
+
const v = validateWeights(policy.weights);
|
|
87
|
+
if (!v.valid)
|
|
88
|
+
return { ok: false, reason: v.errors.join('; ') };
|
|
89
|
+
if (this.policies.has(policy.id))
|
|
90
|
+
return { ok: false, reason: `policy ${policy.id} already registered` };
|
|
91
|
+
this.policies.set(policy.id, policy);
|
|
92
|
+
return { ok: true };
|
|
93
|
+
}
|
|
94
|
+
get(id) { return this.policies.get(id); }
|
|
95
|
+
list() { return [...this.policies.values()]; }
|
|
96
|
+
getActive() { return this.policies.get(this.activeId); }
|
|
97
|
+
get activePolicyId() { return this.activeId; }
|
|
98
|
+
promote(id) {
|
|
99
|
+
if (!this.policies.has(id))
|
|
100
|
+
return false;
|
|
101
|
+
this.activeId = id;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
rollback() { this.activeId = 'default'; }
|
|
105
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/evolution - Router Evolution (P2-05)
|
|
3
|
+
*
|
|
4
|
+
* The audit called for: "让 model routing 本身成为可实验、可回滚策略"
|
|
5
|
+
*
|
|
6
|
+
* Makes routing decisions experimentable: a routing policy can be registered
|
|
7
|
+
* as a candidate, A/B tested against the current policy, and promoted or
|
|
8
|
+
* rolled back based on SPRT results.
|
|
9
|
+
*/
|
|
10
|
+
import type { RouteHistoryEntry } from '@hmharness/kernel';
|
|
11
|
+
export interface RoutingPolicy {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
/** the routing function */
|
|
16
|
+
decide: (history: RouteHistoryEntry[], taskType: string, candidates: string[]) => string;
|
|
17
|
+
}
|
|
18
|
+
export interface RoutingExperiment {
|
|
19
|
+
id: string;
|
|
20
|
+
/** the challenger policy */
|
|
21
|
+
challenger: RoutingPolicy;
|
|
22
|
+
/** incumbent policy (or 'default' for current behavior) */
|
|
23
|
+
incumbent: string;
|
|
24
|
+
/** control arm results */
|
|
25
|
+
controlSuccesses: number;
|
|
26
|
+
controlTotal: number;
|
|
27
|
+
/** treatment arm results */
|
|
28
|
+
treatmentSuccesses: number;
|
|
29
|
+
treatmentTotal: number;
|
|
30
|
+
status: 'running' | 'promoted' | 'rolled-back' | 'inconclusive';
|
|
31
|
+
startedAt: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate a routing experiment using simple SPRT logic.
|
|
35
|
+
* Pure - testable.
|
|
36
|
+
*/
|
|
37
|
+
export declare function evaluateExperiment(exp: RoutingExperiment): {
|
|
38
|
+
decision: 'continue' | 'promote' | 'rollback';
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The routing policy registry - manages candidate and active policies.
|
|
43
|
+
*/
|
|
44
|
+
export declare class RoutingPolicyRegistry {
|
|
45
|
+
private policies;
|
|
46
|
+
private activePolicyId;
|
|
47
|
+
register(policy: RoutingPolicy): {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
reason?: string;
|
|
50
|
+
};
|
|
51
|
+
get(id: string): RoutingPolicy | undefined;
|
|
52
|
+
list(): RoutingPolicy[];
|
|
53
|
+
/** Get the currently active policy */
|
|
54
|
+
getActive(): RoutingPolicy | undefined;
|
|
55
|
+
/** Promote a policy to active (rollback = set back to 'default') */
|
|
56
|
+
promote(policyId: string): boolean;
|
|
57
|
+
/** Rollback to default policy */
|
|
58
|
+
rollback(): void;
|
|
59
|
+
get activeId(): string;
|
|
60
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/evolution - Router Evolution (P2-05)
|
|
3
|
+
*
|
|
4
|
+
* The audit called for: "让 model routing 本身成为可实验、可回滚策略"
|
|
5
|
+
*
|
|
6
|
+
* Makes routing decisions experimentable: a routing policy can be registered
|
|
7
|
+
* as a candidate, A/B tested against the current policy, and promoted or
|
|
8
|
+
* rolled back based on SPRT results.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Evaluate a routing experiment using simple SPRT logic.
|
|
12
|
+
* Pure - testable.
|
|
13
|
+
*/
|
|
14
|
+
export function evaluateExperiment(exp) {
|
|
15
|
+
const minN = 20; // minimum samples per arm
|
|
16
|
+
if (exp.controlTotal < minN || exp.treatmentTotal < minN) {
|
|
17
|
+
return { decision: 'continue', reason: `insufficient samples (control ${exp.controlTotal}/${minN}, treatment ${exp.treatmentTotal}/${minN})` };
|
|
18
|
+
}
|
|
19
|
+
const controlRate = exp.controlSuccesses / exp.controlTotal;
|
|
20
|
+
const treatmentRate = exp.treatmentSuccesses / exp.treatmentTotal;
|
|
21
|
+
const diff = treatmentRate - controlRate;
|
|
22
|
+
// promote if treatment is 5pp better, rollback if 5pp worse
|
|
23
|
+
if (diff >= 0.05) {
|
|
24
|
+
return { decision: 'promote', reason: `treatment ${treatmentRate.toFixed(2)} vs control ${controlRate.toFixed(2)} (+${(diff * 100).toFixed(1)}pp)` };
|
|
25
|
+
}
|
|
26
|
+
if (diff <= -0.05) {
|
|
27
|
+
return { decision: 'rollback', reason: `treatment ${treatmentRate.toFixed(2)} vs control ${controlRate.toFixed(2)} (${(diff * 100).toFixed(1)}pp)` };
|
|
28
|
+
}
|
|
29
|
+
return { decision: 'continue', reason: `diff ${(diff * 100).toFixed(1)}pp within noise band` };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The routing policy registry - manages candidate and active policies.
|
|
33
|
+
*/
|
|
34
|
+
export class RoutingPolicyRegistry {
|
|
35
|
+
policies = new Map();
|
|
36
|
+
activePolicyId = 'default';
|
|
37
|
+
register(policy) {
|
|
38
|
+
if (this.policies.has(policy.id))
|
|
39
|
+
return { ok: false, reason: `policy ${policy.id} already registered` };
|
|
40
|
+
this.policies.set(policy.id, policy);
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
get(id) {
|
|
44
|
+
return this.policies.get(id);
|
|
45
|
+
}
|
|
46
|
+
list() {
|
|
47
|
+
return [...this.policies.values()];
|
|
48
|
+
}
|
|
49
|
+
/** Get the currently active policy */
|
|
50
|
+
getActive() {
|
|
51
|
+
return this.policies.get(this.activePolicyId);
|
|
52
|
+
}
|
|
53
|
+
/** Promote a policy to active (rollback = set back to 'default') */
|
|
54
|
+
promote(policyId) {
|
|
55
|
+
if (!this.policies.has(policyId))
|
|
56
|
+
return false;
|
|
57
|
+
this.activePolicyId = policyId;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/** Rollback to default policy */
|
|
61
|
+
rollback() {
|
|
62
|
+
this.activePolicyId = 'default';
|
|
63
|
+
}
|
|
64
|
+
get activeId() {
|
|
65
|
+
return this.activePolicyId;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/evolution",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.21",
|
|
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",
|