@hmharness/evolution 0.14.21 → 0.14.22

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/index.d.ts CHANGED
@@ -21,3 +21,5 @@ export * from './external.ts';
21
21
  export * from './knowledge-graph.ts';
22
22
  export * from './router-evolution.ts';
23
23
  export * from './context-evolution.ts';
24
+ export * from './rl-governance.ts';
25
+ export * from './policy-optimizer.ts';
package/dist/index.js CHANGED
@@ -21,3 +21,5 @@ export * from "./external.js";
21
21
  export * from "./knowledge-graph.js";
22
22
  export * from "./router-evolution.js";
23
23
  export * from "./context-evolution.js";
24
+ export * from "./rl-governance.js";
25
+ export * from "./policy-optimizer.js";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @hmharness/evolution - Policy Optimization (P3-02)
3
+ *
4
+ * The audit: "把 prompt、skill、tool policy、routing、workflow 全部视为可优化 policy"
5
+ *
6
+ * Provides a unified optimization framework where every configurable aspect
7
+ * of the harness (prompts, skills, tool policies, routing, workflows) is
8
+ * a first-class "Policy" that can be experimented on and improved.
9
+ */
10
+ export type PolicyKind = 'prompt' | 'skill' | 'tool-policy' | 'routing' | 'workflow' | 'context' | 'model';
11
+ export interface OptimizablePolicy {
12
+ id: string;
13
+ kind: PolicyKind;
14
+ name: string;
15
+ /** current value/configuration */
16
+ value: unknown;
17
+ /** human-readable description of what this policy controls */
18
+ description: string;
19
+ /** how changes to this policy are tested */
20
+ evaluationMethod: string;
21
+ }
22
+ export interface PolicyExperiment {
23
+ id: string;
24
+ policy: OptimizablePolicy;
25
+ /** proposed new value */
26
+ challenger: unknown;
27
+ /** A/B arm results */
28
+ controlScores: number[];
29
+ treatmentScores: number[];
30
+ status: 'proposed' | 'running' | 'promoted' | 'rolled-back' | 'rejected';
31
+ createdAt: string;
32
+ }
33
+ export interface PolicyOptimizationReport {
34
+ totalPolicies: number;
35
+ activeExperiments: number;
36
+ promotedCount: number;
37
+ rolledBackCount: number;
38
+ byKind: Record<PolicyKind, number>;
39
+ }
40
+ /**
41
+ * The policy registry - all optimizable policies in one place.
42
+ */
43
+ export declare class PolicyOptimizer {
44
+ private policies;
45
+ private experiments;
46
+ registerPolicy(policy: OptimizablePolicy): {
47
+ ok: boolean;
48
+ reason?: string;
49
+ };
50
+ getPolicy(id: string): OptimizablePolicy | undefined;
51
+ listPolicies(kind?: PolicyKind): OptimizablePolicy[];
52
+ /**
53
+ * Propose an experiment: change a policy's value.
54
+ */
55
+ proposeExperiment(policyId: string, challenger: unknown): {
56
+ ok: boolean;
57
+ experimentId?: string;
58
+ reason?: string;
59
+ };
60
+ /**
61
+ * Record a result for an experiment arm.
62
+ */
63
+ recordResult(experimentId: string, arm: 'control' | 'treatment', score: number): boolean;
64
+ /**
65
+ * Evaluate an experiment (simple mean comparison).
66
+ * Pure logic within the method.
67
+ */
68
+ evaluateExperiment(experimentId: string): {
69
+ decision: string;
70
+ reason: string;
71
+ } | undefined;
72
+ /**
73
+ * Generate a summary report.
74
+ */
75
+ report(): PolicyOptimizationReport;
76
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @hmharness/evolution - Policy Optimization (P3-02)
3
+ *
4
+ * The audit: "把 prompt、skill、tool policy、routing、workflow 全部视为可优化 policy"
5
+ *
6
+ * Provides a unified optimization framework where every configurable aspect
7
+ * of the harness (prompts, skills, tool policies, routing, workflows) is
8
+ * a first-class "Policy" that can be experimented on and improved.
9
+ */
10
+ /**
11
+ * The policy registry - all optimizable policies in one place.
12
+ */
13
+ export class PolicyOptimizer {
14
+ policies = new Map();
15
+ experiments = new Map();
16
+ registerPolicy(policy) {
17
+ if (this.policies.has(policy.id))
18
+ return { ok: false, reason: `policy ${policy.id} already registered` };
19
+ this.policies.set(policy.id, policy);
20
+ return { ok: true };
21
+ }
22
+ getPolicy(id) {
23
+ return this.policies.get(id);
24
+ }
25
+ listPolicies(kind) {
26
+ const all = [...this.policies.values()];
27
+ return kind ? all.filter(p => p.kind === kind) : all;
28
+ }
29
+ /**
30
+ * Propose an experiment: change a policy's value.
31
+ */
32
+ proposeExperiment(policyId, challenger) {
33
+ const policy = this.policies.get(policyId);
34
+ if (!policy)
35
+ return { ok: false, reason: `policy ${policyId} not found` };
36
+ const expId = `exp-${Date.now().toString(36)}`;
37
+ this.experiments.set(expId, {
38
+ id: expId, policy, challenger,
39
+ controlScores: [], treatmentScores: [],
40
+ status: 'proposed', createdAt: new Date().toISOString(),
41
+ });
42
+ return { ok: true, experimentId: expId };
43
+ }
44
+ /**
45
+ * Record a result for an experiment arm.
46
+ */
47
+ recordResult(experimentId, arm, score) {
48
+ const exp = this.experiments.get(experimentId);
49
+ if (!exp)
50
+ return false;
51
+ if (arm === 'control')
52
+ exp.controlScores.push(score);
53
+ else
54
+ exp.treatmentScores.push(score);
55
+ exp.status = 'running';
56
+ return true;
57
+ }
58
+ /**
59
+ * Evaluate an experiment (simple mean comparison).
60
+ * Pure logic within the method.
61
+ */
62
+ evaluateExperiment(experimentId) {
63
+ const exp = this.experiments.get(experimentId);
64
+ if (!exp)
65
+ return undefined;
66
+ const minN = 10;
67
+ if (exp.controlScores.length < minN || exp.treatmentScores.length < minN) {
68
+ return { decision: 'continue', reason: `insufficient samples (ctrl ${exp.controlScores.length}/${minN}, trt ${exp.treatmentScores.length}/${minN})` };
69
+ }
70
+ const ctrlMean = exp.controlScores.reduce((a, b) => a + b, 0) / exp.controlScores.length;
71
+ const trtMean = exp.treatmentScores.reduce((a, b) => a + b, 0) / exp.treatmentScores.length;
72
+ const diff = trtMean - ctrlMean;
73
+ if (diff > 0.02) {
74
+ exp.status = 'promoted';
75
+ exp.policy.value = exp.challenger;
76
+ return { decision: 'promote', reason: `treatment +${diff.toFixed(3)}` };
77
+ }
78
+ if (diff < -0.02) {
79
+ exp.status = 'rolled-back';
80
+ return { decision: 'rollback', reason: `treatment ${diff.toFixed(3)}` };
81
+ }
82
+ return { decision: 'continue', reason: `diff ${diff.toFixed(3)} within noise` };
83
+ }
84
+ /**
85
+ * Generate a summary report.
86
+ */
87
+ report() {
88
+ const all = [...this.policies.values()];
89
+ const exps = [...this.experiments.values()];
90
+ const byKind = {};
91
+ for (const p of all)
92
+ byKind[p.kind] = (byKind[p.kind] ?? 0) + 1;
93
+ return {
94
+ totalPolicies: all.length,
95
+ activeExperiments: exps.filter(e => e.status === 'running').length,
96
+ promotedCount: exps.filter(e => e.status === 'promoted').length,
97
+ rolledBackCount: exps.filter(e => e.status === 'rolled-back').length,
98
+ byKind: byKind,
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @hmharness/evolution - RL Governance (P3-01 + P3-05)
3
+ *
4
+ * The audit: "只有 trajectory 数据经过过滤、去污染、去泄漏、holdout 后再考虑"
5
+ *
6
+ * Formalizes the governance pipeline that ALL training data must pass
7
+ * before entering model fine-tuning. This is the data quality gate.
8
+ */
9
+ export type DecontaminationAction = 'pass' | 'quarantine' | 'reject';
10
+ export interface TrajectoryRecord {
11
+ id: string;
12
+ taskId: string;
13
+ prompt: string;
14
+ output: string;
15
+ outcome: 'ok' | 'degraded' | 'timeout' | 'error';
16
+ tokens: number;
17
+ turns: number;
18
+ /** whether this trajectory came from an external task */
19
+ isExternal: boolean;
20
+ /** whether this trajectory is in the holdout set */
21
+ isHoldout: boolean;
22
+ }
23
+ export interface DecontaminationResult {
24
+ recordId: string;
25
+ action: DecontaminationAction;
26
+ reasons: string[];
27
+ checks: Array<{
28
+ name: string;
29
+ passed: boolean;
30
+ detail: string;
31
+ }>;
32
+ }
33
+ /** Types of leakage to check for */
34
+ export type LeakageType = 'holdout-in-train' | 'self-eval' | 'prompt-duplication' | 'external-in-train';
35
+ export interface LeakageCheck {
36
+ type: LeakageType;
37
+ detected: boolean;
38
+ details: string[];
39
+ }
40
+ /**
41
+ * Filter trajectories by quality before training.
42
+ * Pure - testable.
43
+ */
44
+ export declare function filterForTraining(records: TrajectoryRecord[]): {
45
+ eligible: TrajectoryRecord[];
46
+ rejected: Array<{
47
+ record: TrajectoryRecord;
48
+ reason: string;
49
+ }>;
50
+ };
51
+ /**
52
+ * Detect leakage between training and holdout sets.
53
+ * Pure - testable.
54
+ */
55
+ export declare function detectLeakage(trainingRecords: TrajectoryRecord[], holdoutRecords: TrajectoryRecord[]): LeakageCheck[];
56
+ /**
57
+ * Full decontamination pipeline: run all checks and decide action.
58
+ * Pure - testable.
59
+ */
60
+ export declare function decontaminate(record: TrajectoryRecord, holdoutPrompts: string[]): DecontaminationResult;
61
+ /**
62
+ * Generate an RL readiness report.
63
+ * Pure - testable.
64
+ */
65
+ export declare function rlReadinessReport(params: {
66
+ totalTrajectories: number;
67
+ eligibleForTraining: number;
68
+ holdoutSize: number;
69
+ leakageChecks: LeakageCheck[];
70
+ }): {
71
+ ready: boolean;
72
+ score: number;
73
+ blockers: string[];
74
+ warnings: string[];
75
+ };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @hmharness/evolution - RL Governance (P3-01 + P3-05)
3
+ *
4
+ * The audit: "只有 trajectory 数据经过过滤、去污染、去泄漏、holdout 后再考虑"
5
+ *
6
+ * Formalizes the governance pipeline that ALL training data must pass
7
+ * before entering model fine-tuning. This is the data quality gate.
8
+ */
9
+ /**
10
+ * Filter trajectories by quality before training.
11
+ * Pure - testable.
12
+ */
13
+ export function filterForTraining(records) {
14
+ const eligible = [];
15
+ const rejected = [];
16
+ for (const r of records) {
17
+ if (r.isHoldout) {
18
+ rejected.push({ record: r, reason: 'holdout record must never enter training' });
19
+ continue;
20
+ }
21
+ if (r.isExternal) {
22
+ rejected.push({ record: r, reason: 'external task must stay in holdout' });
23
+ continue;
24
+ }
25
+ if (!r.prompt.trim()) {
26
+ rejected.push({ record: r, reason: 'empty prompt' });
27
+ continue;
28
+ }
29
+ if (!r.output.trim()) {
30
+ rejected.push({ record: r, reason: 'empty output' });
31
+ continue;
32
+ }
33
+ if (r.outcome === 'error') {
34
+ rejected.push({ record: r, reason: 'error outcome not suitable for training' });
35
+ continue;
36
+ }
37
+ eligible.push(r);
38
+ }
39
+ return { eligible, rejected };
40
+ }
41
+ /**
42
+ * Detect leakage between training and holdout sets.
43
+ * Pure - testable.
44
+ */
45
+ export function detectLeakage(trainingRecords, holdoutRecords) {
46
+ const checks = [];
47
+ // 1. holdout-in-train: any holdout record's prompt appears in training
48
+ const holdoutPrompts = new Set(holdoutRecords.map(r => r.prompt.trim().toLowerCase().slice(0, 100)));
49
+ const holdoutInTrain = trainingRecords.filter(r => holdoutPrompts.has(r.prompt.trim().toLowerCase().slice(0, 100)));
50
+ checks.push({
51
+ type: 'holdout-in-train',
52
+ detected: holdoutInTrain.length > 0,
53
+ details: holdoutInTrain.map(r => `training record ${r.id} matches holdout prompt`),
54
+ });
55
+ // 2. self-eval: check if evaluator score correlates suspiciously with self-report
56
+ const selfEval = trainingRecords.filter(r => r.outcome === 'ok' && r.turns <= 1 && r.tokens < 50);
57
+ checks.push({
58
+ type: 'self-eval',
59
+ detected: selfEval.length > 0 && trainingRecords.length > 0 && selfEval.length / trainingRecords.length > 0.8,
60
+ details: selfEval.length > 0 && selfEval.length / trainingRecords.length > 0.8
61
+ ? [`${selfEval.length}/${trainingRecords.length} records are trivially self-reported (1 turn, <50 tokens)`]
62
+ : [],
63
+ });
64
+ // 3. prompt-duplication within training set
65
+ const promptCounts = new Map();
66
+ for (const r of trainingRecords) {
67
+ const key = r.prompt.trim().toLowerCase().slice(0, 100);
68
+ promptCounts.set(key, (promptCounts.get(key) ?? 0) + 1);
69
+ }
70
+ const dupes = [...promptCounts.entries()].filter(([, n]) => n > 5);
71
+ checks.push({
72
+ type: 'prompt-duplication',
73
+ detected: dupes.length > 0,
74
+ details: dupes.map(([k, n]) => `prompt "${k.slice(0, 40)}..." appears ${n} times`),
75
+ });
76
+ // 4. external-in-train
77
+ const externalInTrain = trainingRecords.filter(r => r.isExternal);
78
+ checks.push({
79
+ type: 'external-in-train',
80
+ detected: externalInTrain.length > 0,
81
+ details: externalInTrain.map(r => `external record ${r.id} found in training set`),
82
+ });
83
+ return checks;
84
+ }
85
+ /**
86
+ * Full decontamination pipeline: run all checks and decide action.
87
+ * Pure - testable.
88
+ */
89
+ export function decontaminate(record, holdoutPrompts) {
90
+ const checks = [];
91
+ const reasons = [];
92
+ // holdout check
93
+ const promptKey = record.prompt.trim().toLowerCase().slice(0, 100);
94
+ const inHoldout = holdoutPrompts.some(p => p.trim().toLowerCase().slice(0, 100) === promptKey);
95
+ checks.push({ name: 'holdout-overlap', passed: !inHoldout, detail: inHoldout ? 'prompt matches holdout' : 'clear' });
96
+ if (inHoldout)
97
+ reasons.push('record overlaps with holdout set');
98
+ // quality check
99
+ const qualityOk = record.prompt.trim().length > 0 && record.output.trim().length > 0;
100
+ checks.push({ name: 'quality', passed: qualityOk, detail: qualityOk ? 'non-empty' : 'empty prompt/output' });
101
+ if (!qualityOk)
102
+ reasons.push('empty prompt or output');
103
+ // outcome check
104
+ const outcomeOk = record.outcome !== 'error';
105
+ checks.push({ name: 'outcome', passed: outcomeOk, detail: `outcome=${record.outcome}` });
106
+ if (!outcomeOk)
107
+ reasons.push('error outcome');
108
+ // external check
109
+ const externalOk = !record.isExternal;
110
+ checks.push({ name: 'external', passed: externalOk, detail: record.isExternal ? 'is external' : 'internal' });
111
+ if (!externalOk)
112
+ reasons.push('external record must stay in holdout');
113
+ const action = reasons.length === 0 ? 'pass' : reasons.some(r => r.includes('holdout') || r.includes('external')) ? 'reject' : 'quarantine';
114
+ return { recordId: record.id, action, reasons, checks };
115
+ }
116
+ /**
117
+ * Generate an RL readiness report.
118
+ * Pure - testable.
119
+ */
120
+ export function rlReadinessReport(params) {
121
+ const blockers = [];
122
+ const warnings = [];
123
+ if (params.eligibleForTraining < 100)
124
+ blockers.push(`only ${params.eligibleForTraining} eligible trajectories (need >=100)`);
125
+ if (params.holdoutSize < 10)
126
+ blockers.push(`holdout too small (${params.holdoutSize}, need >=10)`);
127
+ const criticalLeakage = params.leakageChecks.filter(c => c.detected && (c.type === 'holdout-in-train' || c.type === 'external-in-train'));
128
+ if (criticalLeakage.length > 0)
129
+ blockers.push(`critical leakage detected: ${criticalLeakage.map(c => c.type).join(', ')}`);
130
+ for (const c of params.leakageChecks) {
131
+ if (c.detected && !blockers.some(b => b.includes(c.type)))
132
+ warnings.push(`${c.type}: ${c.details.join('; ')}`);
133
+ }
134
+ const score = Math.max(0, 100 - blockers.length * 30 - warnings.length * 10);
135
+ return { ready: blockers.length === 0, score, blockers, warnings };
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/evolution",
3
- "version": "0.14.21",
3
+ "version": "0.14.22",
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",