@hmharness/evolution 0.1.0

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.
@@ -0,0 +1,38 @@
1
+ export interface BenchCase {
2
+ name: string;
3
+ prompt: string;
4
+ /** All substrings (split on &&) must appear in the output, case-insensitive. */
5
+ expect: string[];
6
+ /** Output must equal this string exactly (both trimmed). */
7
+ expectExact?: string;
8
+ /** Output must match this regex (whole output, case-sensitive). */
9
+ expectRegex?: string;
10
+ /** Substrings that must NOT appear (failure-marker exclusion). */
11
+ expectNone?: string[];
12
+ /** At least ONE of these substrings must appear. */
13
+ expectAny?: string[];
14
+ /** When true the case runs through the full agent loop with tools. */
15
+ tools: boolean;
16
+ /** Holdout cases are excluded from the promotion gate and re-verify after promotion (anti-memorization, GDPevo style). */
17
+ holdout: boolean;
18
+ /** Candidate token-cost ceiling as a multiple of the baseline run. */
19
+ costCap?: number;
20
+ }
21
+ export interface BenchResult {
22
+ name: string;
23
+ pass: boolean;
24
+ detail: string;
25
+ }
26
+ export declare function matchExpect(output: string, expect: string[]): boolean;
27
+ /** Full structured assertion: every declared mode must hold. */
28
+ export declare function matchCase(output: string, c: Pick<BenchCase, 'expect' | 'expectExact' | 'expectRegex' | 'expectNone' | 'expectAny'>): {
29
+ pass: boolean;
30
+ detail: string;
31
+ };
32
+ export declare function listCases(home: string): Promise<BenchCase[]>;
33
+ export declare function runBench(home: string, run: (c: BenchCase) => Promise<string>): Promise<{
34
+ results: BenchResult[];
35
+ passRate: number;
36
+ }>;
37
+ /** Starter cases so the evolve gate has a signal from day one. */
38
+ export declare function seedCases(home: string): Promise<string[]>;
package/dist/bench.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @hmharness/evolution - bench
3
+ * The fitness signal for self-evolution. bench/cases/*.task files are:
4
+ * line 1 the prompt
5
+ * expect: a && b substrings that must ALL appear in the final output
6
+ * expect-exact: "..." the output equals this string (trimmed)
7
+ * expect-regex: ... a regex the output must match
8
+ * expect-none: a && b substrings that must NOT appear
9
+ * expect-any: a || b one of these substrings must appear
10
+ * tools: loop (optional) run through the full agent loop with tools
11
+ * cost-cap: 1.3 (optional) candidate cost multiplier ceiling vs baseline
12
+ * Skill or prompt changes must keep the bench green before promotion - the
13
+ * evolve loop enforces this A/B (baseline vs candidate).
14
+ *
15
+ * Assertion upgrade (gate methodology): plain substring matching lets
16
+ * verbose-but-wrong outputs pass; the structured modes pin exact shapes,
17
+ * forbid failure markers, and (with cost-cap) stop a candidate that only
18
+ * passes by burning 3x tokens. Old files keep working - `expect:` keeps
19
+ * its ALL-substrings semantics.
20
+ */
21
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
22
+ import { join } from 'node:path';
23
+ export function matchExpect(output, expect) {
24
+ const lower = output.toLowerCase();
25
+ return expect.every((e) => lower.includes(e.toLowerCase()));
26
+ }
27
+ /** Full structured assertion: every declared mode must hold. */
28
+ export function matchCase(output, c) {
29
+ if (c.expectExact !== undefined && output.trim() !== c.expectExact.trim()) {
30
+ return { pass: false, detail: `exact mismatch: got "${output.trim().slice(0, 80)}"` };
31
+ }
32
+ if (c.expectRegex !== undefined) {
33
+ try {
34
+ if (!new RegExp(c.expectRegex).test(output))
35
+ return { pass: false, detail: `regex mismatch: /${c.expectRegex.slice(0, 60)}/` };
36
+ }
37
+ catch {
38
+ return { pass: false, detail: `invalid regex in case: ${c.expectRegex.slice(0, 40)}` };
39
+ }
40
+ }
41
+ if (c.expectNone && c.expectNone.some((e) => output.toLowerCase().includes(e.toLowerCase()))) {
42
+ return { pass: false, detail: `forbidden marker present: ${c.expectNone.join(' && ')}` };
43
+ }
44
+ if (c.expectAny && !c.expectAny.some((e) => output.toLowerCase().includes(e.toLowerCase()))) {
45
+ return { pass: false, detail: `none of the allowed markers found: ${c.expectAny.join(' || ')}` };
46
+ }
47
+ if (!matchExpect(output, c.expect)) {
48
+ return { pass: false, detail: `missing "${c.expect.join('" && "')}" in output` };
49
+ }
50
+ return { pass: true, detail: 'ok' };
51
+ }
52
+ export async function listCases(home) {
53
+ const dir = join(home, 'bench', 'cases');
54
+ let files;
55
+ try {
56
+ files = (await readdir(dir)).filter((f) => f.endsWith('.task'));
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ const cases = [];
62
+ for (const f of files.sort()) {
63
+ const raw = (await readFile(join(dir, f), 'utf8')).trim();
64
+ const lines = raw.split('\n');
65
+ const prompt = lines[0];
66
+ const pick = (key) => {
67
+ const l = lines.find((x) => x.startsWith(key + ':'));
68
+ return l ? l.slice(key.length + 1).trim() : undefined;
69
+ };
70
+ const list = (key) => {
71
+ const v = pick(key);
72
+ return v ? v.split(/&&|\|\|/).map((s) => s.trim()).filter(Boolean) : undefined;
73
+ };
74
+ const costCap = Number(pick('cost-cap'));
75
+ cases.push({
76
+ name: f.replace(/\.task$/, ''),
77
+ prompt,
78
+ expect: list('expect') ?? [],
79
+ expectExact: pick('expect-exact')?.replace(/^"|"$/g, ''),
80
+ expectRegex: pick('expect-regex'),
81
+ expectNone: list('expect-none'),
82
+ expectAny: list('expect-any'),
83
+ tools: pick('tools') === 'loop',
84
+ holdout: pick('holdout') === 'true',
85
+ costCap: Number.isFinite(costCap) && costCap > 0 ? costCap : undefined,
86
+ });
87
+ }
88
+ return cases;
89
+ }
90
+ export async function runBench(home, run) {
91
+ const cases = await listCases(home);
92
+ const results = [];
93
+ for (const c of cases) {
94
+ try {
95
+ const out = await run(c);
96
+ const r = matchCase(out, c);
97
+ results.push({ name: c.name, pass: r.pass, detail: r.detail });
98
+ }
99
+ catch (err) {
100
+ results.push({ name: c.name, pass: false, detail: String(err).slice(0, 120) });
101
+ }
102
+ }
103
+ const passed = results.filter((r) => r.pass).length;
104
+ return { results, passRate: cases.length === 0 ? 1 : passed / cases.length };
105
+ }
106
+ /** Starter cases so the evolve gate has a signal from day one. */
107
+ export async function seedCases(home) {
108
+ const dir = join(home, 'bench', 'cases');
109
+ const written = [];
110
+ const seeds = [
111
+ ['toolchain-report.task', '运行鸿蒙工具链体检,然后逐项说出一共检查了哪三个工具的名称\nexpect: hdc && hvigorw && ohpm\ntools: loop\n'],
112
+ ['reply-determinism.task', '只回复这六个字符,不要任何其他内容:HMH-OK\nexpect: HMH-OK\n'],
113
+ ['toolchain-ohpm-status.task', '运行鸿蒙工具链体检,然后只回答 ohpm 的状态是 OK 还是 MISSING\nexpect: OK\ntools: loop\nholdout: true\n'],
114
+ ];
115
+ let existing = [];
116
+ try {
117
+ existing = await readdir(dir);
118
+ }
119
+ catch {
120
+ /* first seed */
121
+ }
122
+ for (const [name, body] of seeds) {
123
+ if (existing.includes(name))
124
+ continue;
125
+ await mkdir(dir, { recursive: true });
126
+ await writeFile(join(dir, name), body, 'utf8');
127
+ written.push(name);
128
+ }
129
+ return written;
130
+ }
@@ -0,0 +1,89 @@
1
+ import { type ProviderConfig } from '@hmharness/kernel';
2
+ import { type BenchCase } from './bench.ts';
3
+ export interface SkillProposal {
4
+ name: string;
5
+ description: string;
6
+ skill_md: string;
7
+ }
8
+ export interface ProposalOutcome {
9
+ name: string;
10
+ action: 'promoted' | 'rejected' | 'error';
11
+ reason: string;
12
+ baseline?: {
13
+ passRate: number;
14
+ cases: string;
15
+ };
16
+ candidate?: {
17
+ passRate: number;
18
+ cases: string;
19
+ };
20
+ holdout?: {
21
+ baselineRate: number;
22
+ candidateRate: number;
23
+ };
24
+ /** P0 lineage ledger: what fed this decision (provenance for impact
25
+ * attribution - which insights produced which skill, with which scores).
26
+ * Evolution-artifact genealogy is the GEPA/DGM archive lesson: decisions
27
+ * without ancestry cannot be audited for objective hacking. */
28
+ lineage?: {
29
+ parentInsights: string[];
30
+ scores: {
31
+ train: number;
32
+ holdout?: number;
33
+ };
34
+ metaModel: string;
35
+ decidedAt: string;
36
+ };
37
+ }
38
+ export interface EvolveReport {
39
+ time: string;
40
+ model: string;
41
+ seededCases: string[];
42
+ insightCount: number;
43
+ noteCount: number;
44
+ proposals: SkillProposal[];
45
+ outcomes: ProposalOutcome[];
46
+ memoryDistilled: string | null;
47
+ /** code-level evolution: proposed patches (DGM bridge) */
48
+ codePatches?: Array<{
49
+ name: string;
50
+ file: string;
51
+ reason: string;
52
+ }>;
53
+ /** code-level outcomes: merged/reverted/error per patch */
54
+ patchOutcomes?: Array<{
55
+ name: string;
56
+ action: string;
57
+ reason: string;
58
+ branch?: string;
59
+ }>;
60
+ /** P0 impact attribution: canary A/B comparison applied this cycle */
61
+ impact?: {
62
+ rows: Array<{
63
+ skill: string;
64
+ exposed: string;
65
+ control: string;
66
+ verdict: string;
67
+ }>;
68
+ applied: string[];
69
+ };
70
+ /** P1 lifecycle: skills moved to dormant this cycle */
71
+ decayed?: string[];
72
+ /** budget state at cycle start (observability) */
73
+ budget?: {
74
+ cyclesToday: number;
75
+ maxCyclesPerDay?: number;
76
+ };
77
+ }
78
+ /** Runs one bench case with the given skills block injected. */
79
+ export type CaseRunner = (c: BenchCase, skillsPrompt: string) => Promise<string>;
80
+ export declare function runEvolution(opts: {
81
+ home: string;
82
+ provider: ProviderConfig;
83
+ runCase: CaseRunner;
84
+ maxProposals?: number;
85
+ /** Skip the meta-model call and evaluate these proposals directly (tests / future UIs). */
86
+ presetProposals?: SkillProposal[];
87
+ log?: (line: string) => void;
88
+ }): Promise<EvolveReport>;
89
+ export declare function screenForPoison(text: string): string | null;