@dzhechkov/harness-core 0.3.117 → 0.3.119

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,88 @@
1
+ /**
2
+ * Learned cost-optimal model routing (feature learned-cost-routing, ADR-001) — the `auto-cost` spec.
3
+ *
4
+ * A per-stage model router that learns which model actually SUCCEEDS at a stage (fewest retries = lowest true
5
+ * cost). The SELECTOR is PURE over an injected store snapshot (mirrors the shipped rUv `MetaHarnessRouter`,
6
+ * `open-claude-code/v2/src/optimize/router.mjs`: cheapest model clearing a 0.7 success bar, else a cheapest-
7
+ * first chain for escalate-on-fail — the `cve-bench/.../model-chain.mjs` pattern). The store is a thin JSON
8
+ * layer under `.dz/` (top-level ESM fs; a lazy require() is undefined at runtime — the R1 footgun).
9
+ *
10
+ * Storage is JSON, not SQLite: `better-sqlite3` is not a harness-core dep, `.dz/` already persists JSON state,
11
+ * and the grounded reference is emphatically zero-dependency (ADR-073 "pure-TS path is dependency-free").
12
+ *
13
+ * SAFETY PROPERTIES (ADR-001, load-bearing, each pinned by a test):
14
+ * 1. §3 — `selectAutoCost('qe', …, {family})` ranks ONLY the cross-family of the coder → a model that wrote
15
+ * code can NEVER self-QE (the named cross-model-QE invariant).
16
+ * 2. §2 — a stage that PRODUCED an artifact but FAILED the downstream gate is recorded as a FAILURE at its
17
+ * key (`finalizeOutcome(..., false)`), down-ranking that model — success ≠ "returned something".
18
+ * 3. §1 — the same store snapshot yields the same pick (deterministic); no `auto-cost` spec ⇒ nothing here
19
+ * is touched (byte-identical, opt-in).
20
+ */
21
+ export type Family = 'claude' | 'openai';
22
+ export interface ModelRung {
23
+ readonly id: string;
24
+ readonly costRank: number;
25
+ readonly family: Family;
26
+ }
27
+ /**
28
+ * KNOWN model set, cheapest→dearest. `costRank` is a COARSE relative ordering (an estimate from conservative
29
+ * public list prices — the router.mjs precedent, "not fabricated metrics"); only the ORDER is load-bearing.
30
+ * gpt-5.6-ready: adding a rung is a data-only edit. Kept consistent with KNOWN_CODEX / CLAUDE_NAMES in
31
+ * feature-adr-routing.ts.
32
+ */
33
+ export declare const COST_LADDER: readonly ModelRung[];
34
+ export interface OutcomeStats {
35
+ readonly attempts: number;
36
+ readonly successes: number;
37
+ readonly successRate: number;
38
+ }
39
+ export type StatsFor = (model: string) => OutcomeStats;
40
+ export interface AutoCostOpts {
41
+ readonly ladder?: readonly ModelRung[];
42
+ readonly qualityBar?: number;
43
+ readonly minSamples?: number;
44
+ readonly family?: Family;
45
+ }
46
+ export interface AutoCostPick {
47
+ readonly model: string;
48
+ readonly chain: readonly string[];
49
+ readonly evidence: string;
50
+ readonly metBar: boolean;
51
+ }
52
+ /**
53
+ * PURE selection (ADR §1). Strategy (b), bar 0.7: pick the CHEAPEST model that has ≥`minSamples` attempts AND
54
+ * a learned success-rate ≥ bar at this key; if NONE has proven itself, return the cheapest rung (cold-start)
55
+ * with the full cheapest-first `chain` so the caller escalates on real failure (never a pre-emptive jump to a
56
+ * dear model). Deterministic given the snapshot. `family` restricts the ladder (qe cross-family guard).
57
+ */
58
+ export declare function selectAutoCost(stage: string, tier: string, statsFor: StatsFor, opts?: AutoCostOpts): AutoCostPick;
59
+ /** The next rung after a failed model in the chain (FR-5 escalate-on-fail); null at the top. */
60
+ export declare function nextInChain(chain: readonly string[], failedModel: string): string | null;
61
+ export interface OutcomeRow {
62
+ attempts: number;
63
+ successes: number;
64
+ provisional?: number;
65
+ }
66
+ export interface OutcomeStore {
67
+ readonly rows: Record<string, OutcomeRow>;
68
+ }
69
+ export declare const ROUTING_OUTCOMES_PATH = ".dz/routing-outcomes.json";
70
+ /** Load the outcome store; absent/corrupt/unreadable → empty (never throws). */
71
+ export declare function loadOutcomes(repoRoot: string): OutcomeStore;
72
+ /** Build the injected StatsFor for a (stage, tier) from a loaded snapshot. PURE. */
73
+ export declare function statsForKey(store: OutcomeStore, stage: string, tier: string): StatsFor;
74
+ /**
75
+ * Record a PROVISIONAL outcome (i): the stage produced a non-empty artifact / did not die. Bumps `provisional`
76
+ * and, for a gate-less stage, this weak signal counts toward attempts+successes (weak-provisional decision).
77
+ * `weakCredit=false` for gated stages — the real (ii) credit lands in finalizeOutcome.
78
+ */
79
+ export declare function recordProvisional(repoRoot: string, stage: string, tier: string, model: string, weakCredit?: boolean): void;
80
+ /**
81
+ * Finalize the AUTHORITATIVE outcome (ii) from the downstream gate, attributed back to the (stage, model)
82
+ * that produced the artifact. A produced-but-gate-FAILED run records a FAILURE (attempts+1, successes+0) —
83
+ * success is NOT "returned something" (ADR §2, load-bearing).
84
+ */
85
+ export declare function finalizeOutcome(repoRoot: string, stage: string, tier: string, model: string, success: boolean): void;
86
+ /** Human-readable learned table for `dz routing`. Deterministic (sorted). */
87
+ export declare function renderOutcomes(store: OutcomeStore, filterStage?: string): string;
88
+ //# sourceMappingURL=routing-outcomes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-outcomes.d.ts","sourceRoot":"","sources":["../src/routing-outcomes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAQ1C,CAAC;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AACD,MAAM,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAaD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,GAAE,YAAiB,GAAG,YAAY,CAiCrH;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIxF;AAID,MAAM,WAAW,UAAU;IAAG,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE;AACzF,MAAM,WAAW,YAAY;IAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CAAE;AAE3E,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AAGjE,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAyB3D;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAMtF;AAuBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,UAAQ,GAAG,IAAI,CAMxH;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAMpH;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBhF"}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Learned cost-optimal model routing (feature learned-cost-routing, ADR-001) — the `auto-cost` spec.
3
+ *
4
+ * A per-stage model router that learns which model actually SUCCEEDS at a stage (fewest retries = lowest true
5
+ * cost). The SELECTOR is PURE over an injected store snapshot (mirrors the shipped rUv `MetaHarnessRouter`,
6
+ * `open-claude-code/v2/src/optimize/router.mjs`: cheapest model clearing a 0.7 success bar, else a cheapest-
7
+ * first chain for escalate-on-fail — the `cve-bench/.../model-chain.mjs` pattern). The store is a thin JSON
8
+ * layer under `.dz/` (top-level ESM fs; a lazy require() is undefined at runtime — the R1 footgun).
9
+ *
10
+ * Storage is JSON, not SQLite: `better-sqlite3` is not a harness-core dep, `.dz/` already persists JSON state,
11
+ * and the grounded reference is emphatically zero-dependency (ADR-073 "pure-TS path is dependency-free").
12
+ *
13
+ * SAFETY PROPERTIES (ADR-001, load-bearing, each pinned by a test):
14
+ * 1. §3 — `selectAutoCost('qe', …, {family})` ranks ONLY the cross-family of the coder → a model that wrote
15
+ * code can NEVER self-QE (the named cross-model-QE invariant).
16
+ * 2. §2 — a stage that PRODUCED an artifact but FAILED the downstream gate is recorded as a FAILURE at its
17
+ * key (`finalizeOutcome(..., false)`), down-ranking that model — success ≠ "returned something".
18
+ * 3. §1 — the same store snapshot yields the same pick (deterministic); no `auto-cost` spec ⇒ nothing here
19
+ * is touched (byte-identical, opt-in).
20
+ */
21
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
22
+ import { join, dirname } from 'node:path';
23
+ /**
24
+ * KNOWN model set, cheapest→dearest. `costRank` is a COARSE relative ordering (an estimate from conservative
25
+ * public list prices — the router.mjs precedent, "not fabricated metrics"); only the ORDER is load-bearing.
26
+ * gpt-5.6-ready: adding a rung is a data-only edit. Kept consistent with KNOWN_CODEX / CLAUDE_NAMES in
27
+ * feature-adr-routing.ts.
28
+ */
29
+ export const COST_LADDER = Object.freeze([
30
+ Object.freeze({ id: 'haiku', costRank: 1, family: 'claude' }),
31
+ Object.freeze({ id: 'fable', costRank: 1, family: 'claude' }),
32
+ Object.freeze({ id: 'gpt-5.5', costRank: 2, family: 'openai' }),
33
+ Object.freeze({ id: 'sonnet', costRank: 2, family: 'claude' }),
34
+ Object.freeze({ id: 'gpt-5.6', costRank: 3, family: 'openai' }),
35
+ Object.freeze({ id: 'gpt-5.6-sol', costRank: 3, family: 'openai' }),
36
+ Object.freeze({ id: 'opus', costRank: 4, family: 'claude' }),
37
+ ]);
38
+ const DEFAULT_BAR = 0.7;
39
+ const DEFAULT_MIN_SAMPLES = 3;
40
+ /** Cheapest-first, deterministic order: by costRank, tie-broken by id (stable). */
41
+ function orderedLadder(ladder, family) {
42
+ return ladder
43
+ .filter((r) => family === undefined || r.family === family)
44
+ .slice()
45
+ .sort((a, b) => a.costRank - b.costRank || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
46
+ }
47
+ /**
48
+ * PURE selection (ADR §1). Strategy (b), bar 0.7: pick the CHEAPEST model that has ≥`minSamples` attempts AND
49
+ * a learned success-rate ≥ bar at this key; if NONE has proven itself, return the cheapest rung (cold-start)
50
+ * with the full cheapest-first `chain` so the caller escalates on real failure (never a pre-emptive jump to a
51
+ * dear model). Deterministic given the snapshot. `family` restricts the ladder (qe cross-family guard).
52
+ */
53
+ export function selectAutoCost(stage, tier, statsFor, opts = {}) {
54
+ const bar = opts.qualityBar ?? DEFAULT_BAR;
55
+ const minSamples = opts.minSamples ?? DEFAULT_MIN_SAMPLES;
56
+ const rungs = orderedLadder(opts.ladder ?? COST_LADDER, opts.family);
57
+ const chain = rungs.map((r) => r.id);
58
+ if (rungs.length === 0) {
59
+ return { model: '', chain: [], evidence: `auto-cost(${stage}/${tier}): no candidate models`, metBar: false };
60
+ }
61
+ // Pass 1 — a PROVEN-GOOD model (≥minSamples, rate ≥ bar): cheapest wins.
62
+ for (const r of rungs) {
63
+ const s = statsFor(r.id);
64
+ if (s.attempts >= minSamples && s.successRate >= bar) {
65
+ const pct = (s.successRate * 100).toFixed(0);
66
+ return { model: r.id, chain, evidence: `${r.id} (auto-cost: ${pct}% / ${s.attempts} runs @ ${stage}/${tier})`, metBar: true };
67
+ }
68
+ }
69
+ // Pass 2 — cross-run escalate-on-fail: SKIP a KNOWN-BAD rung (enough samples, rate < bar) so a repeatedly
70
+ // failing cheapest model is not re-selected forever. Pick the cheapest rung that is untried-or-thin.
71
+ const isKnownBad = (id) => {
72
+ const s = statsFor(id);
73
+ return s.attempts >= minSamples && s.successRate < bar;
74
+ };
75
+ for (const r of rungs) {
76
+ if (!isKnownBad(r.id)) {
77
+ const s = statsFor(r.id);
78
+ const note = s.attempts > 0 ? `${s.successes}/${s.attempts} so far` : 'untried';
79
+ return { model: r.id, chain, evidence: `${r.id} (auto-cost: cold-start [${note}], chain ${chain.join('→')} @ ${stage}/${tier})`, metBar: false };
80
+ }
81
+ }
82
+ // Pass 3 — every rung is known-bad: fall back to the LEAST-bad (highest rate, tie → cheapest via stable order).
83
+ const leastBad = [...rungs].sort((a, b) => statsFor(b.id).successRate - statsFor(a.id).successRate)[0];
84
+ const lb = statsFor(leastBad.id);
85
+ return { model: leastBad.id, chain, evidence: `${leastBad.id} (auto-cost: all rungs under bar; least-bad ${(lb.successRate * 100).toFixed(0)}% @ ${stage}/${tier})`, metBar: false };
86
+ }
87
+ /** The next rung after a failed model in the chain (FR-5 escalate-on-fail); null at the top. */
88
+ export function nextInChain(chain, failedModel) {
89
+ const i = chain.indexOf(failedModel);
90
+ if (i === -1 || i >= chain.length - 1)
91
+ return null;
92
+ return chain[i + 1] ?? null;
93
+ }
94
+ export const ROUTING_OUTCOMES_PATH = '.dz/routing-outcomes.json';
95
+ const keyOf = (stage, tier, model) => `${stage}|${tier}|${model}`;
96
+ /** Load the outcome store; absent/corrupt/unreadable → empty (never throws). */
97
+ export function loadOutcomes(repoRoot) {
98
+ try {
99
+ const p = join(repoRoot, ROUTING_OUTCOMES_PATH);
100
+ if (!existsSync(p))
101
+ return { rows: {} };
102
+ const parsed = JSON.parse(readFileSync(p, 'utf8'));
103
+ if (parsed === null || typeof parsed !== 'object')
104
+ return { rows: {} };
105
+ const rows = parsed.rows;
106
+ if (rows === null || typeof rows !== 'object')
107
+ return { rows: {} };
108
+ // sanitize: keep only well-formed numeric rows
109
+ const clean = {};
110
+ for (const [k, v] of Object.entries(rows)) {
111
+ if (v && typeof v === 'object') {
112
+ const r = v;
113
+ // Number.isFinite rejects Infinity/NaN (1e400 JSON-parses to Infinity, which is a number ≥ 0 and would
114
+ // otherwise pass, then Infinity/Infinity → NaN poisons successRate). Floor to a non-negative integer.
115
+ const num = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
116
+ const a = num(r.attempts);
117
+ const s = Math.min(num(r.successes), a);
118
+ clean[k] = { attempts: a, successes: s, ...(typeof r.provisional === 'number' && Number.isFinite(r.provisional) && r.provisional >= 0 ? { provisional: Math.floor(r.provisional) } : {}) };
119
+ }
120
+ }
121
+ return { rows: clean };
122
+ }
123
+ catch {
124
+ return { rows: {} };
125
+ }
126
+ }
127
+ /** Build the injected StatsFor for a (stage, tier) from a loaded snapshot. PURE. */
128
+ export function statsForKey(store, stage, tier) {
129
+ return (model) => {
130
+ const r = store.rows[keyOf(stage, tier, model)];
131
+ if (!r || r.attempts <= 0)
132
+ return { attempts: 0, successes: 0, successRate: 0 };
133
+ return { attempts: r.attempts, successes: r.successes, successRate: r.successes / r.attempts };
134
+ };
135
+ }
136
+ function writeStore(repoRoot, store) {
137
+ try {
138
+ const p = join(repoRoot, ROUTING_OUTCOMES_PATH);
139
+ mkdirSync(dirname(p), { recursive: true });
140
+ // Atomic write: a temp file + rename can't leave a half-written/corrupt store if the process dies mid-write
141
+ // (rename is atomic on the same filesystem). Cross-PROCESS concurrent writers can still lose an update —
142
+ // that is an accepted degradation (the feature-adr workflow records sequentially; see architecture/degradations.md).
143
+ const tmp = p + '.tmp-' + process.pid;
144
+ writeFileSync(tmp, JSON.stringify({ rows: store.rows }, null, 2) + '\n');
145
+ try {
146
+ renameSync(tmp, p);
147
+ }
148
+ catch (e) {
149
+ // rename failed (e.g. dest is a directory / cross-device) — do NOT orphan the temp file (QE #4).
150
+ try {
151
+ unlinkSync(tmp);
152
+ }
153
+ catch { /* best-effort cleanup */ }
154
+ throw e;
155
+ }
156
+ }
157
+ catch {
158
+ /* non-blocking: learning is advisory */
159
+ }
160
+ }
161
+ /**
162
+ * Record a PROVISIONAL outcome (i): the stage produced a non-empty artifact / did not die. Bumps `provisional`
163
+ * and, for a gate-less stage, this weak signal counts toward attempts+successes (weak-provisional decision).
164
+ * `weakCredit=false` for gated stages — the real (ii) credit lands in finalizeOutcome.
165
+ */
166
+ export function recordProvisional(repoRoot, stage, tier, model, weakCredit = false) {
167
+ const store = loadOutcomes(repoRoot);
168
+ const k = keyOf(stage, tier, model);
169
+ const r = store.rows[k] ?? { attempts: 0, successes: 0 };
170
+ const next = { attempts: r.attempts + (weakCredit ? 1 : 0), successes: r.successes + (weakCredit ? 1 : 0), provisional: (r.provisional ?? 0) + 1 };
171
+ writeStore(repoRoot, { rows: { ...store.rows, [k]: next } });
172
+ }
173
+ /**
174
+ * Finalize the AUTHORITATIVE outcome (ii) from the downstream gate, attributed back to the (stage, model)
175
+ * that produced the artifact. A produced-but-gate-FAILED run records a FAILURE (attempts+1, successes+0) —
176
+ * success is NOT "returned something" (ADR §2, load-bearing).
177
+ */
178
+ export function finalizeOutcome(repoRoot, stage, tier, model, success) {
179
+ const store = loadOutcomes(repoRoot);
180
+ const k = keyOf(stage, tier, model);
181
+ const r = store.rows[k] ?? { attempts: 0, successes: 0 };
182
+ const next = { attempts: r.attempts + 1, successes: r.successes + (success ? 1 : 0), ...(r.provisional !== undefined ? { provisional: r.provisional } : {}) };
183
+ writeStore(repoRoot, { rows: { ...store.rows, [k]: next } });
184
+ }
185
+ /** Human-readable learned table for `dz routing`. Deterministic (sorted). */
186
+ export function renderOutcomes(store, filterStage) {
187
+ const keys = Object.keys(store.rows).sort();
188
+ const shown = keys.filter((k) => filterStage === undefined || k.startsWith(filterStage + '|'));
189
+ if (shown.length === 0)
190
+ return filterStage ? `No learned routing outcomes for stage "${filterStage}".` : 'No learned routing outcomes yet (auto-cost has not run, or no gate has resolved).';
191
+ const lines = ['Learned routing outcomes (what `auto-cost` currently believes):', ''];
192
+ let lastStage = '';
193
+ for (const k of shown) {
194
+ const r = store.rows[k];
195
+ if (!r)
196
+ continue;
197
+ const [stage = '', tier = '', model = ''] = k.split('|');
198
+ if (stage !== lastStage) {
199
+ lines.push(`## ${stage}`);
200
+ lastStage = stage;
201
+ }
202
+ const rate = r.attempts > 0 ? ((r.successes / r.attempts) * 100).toFixed(0) + '%' : 'n/a';
203
+ const prov = r.provisional ? `, ${r.provisional} provisional` : '';
204
+ lines.push(` ${tier.padEnd(3)} ${model.padEnd(14)} ${rate.padStart(4)} (${r.successes}/${r.attempts} gated${prov})`);
205
+ }
206
+ return lines.join('\n');
207
+ }
208
+ //# sourceMappingURL=routing-outcomes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-outcomes.js","sourceRoot":"","sources":["../src/routing-outcomes.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-core",
3
- "version": "0.3.117",
3
+ "version": "0.3.119",
4
4
  "description": "Shared harness logic - skill loading, additive apply, and the init/sync/verify/doctor operations.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,12 +31,12 @@
31
31
  "@dzhechkov/adapter-opencode": "^0.2.0",
32
32
  "yaml": "^2.0.0",
33
33
  "@dzhechkov/adapter-agents-md": "0.1.1",
34
+ "@dzhechkov/adapter-cursor": "0.1.1",
34
35
  "@dzhechkov/adapter-copilot": "0.1.1",
35
- "@dzhechkov/adapter-windsurf": "0.1.1",
36
36
  "@dzhechkov/adapter-gemini": "0.1.1",
37
- "@dzhechkov/adapter-cursor": "0.1.1",
38
- "@dzhechkov/memory": "0.2.9",
39
- "@dzhechkov/core": "0.2.14"
37
+ "@dzhechkov/adapter-windsurf": "0.1.1",
38
+ "@dzhechkov/core": "0.2.14",
39
+ "@dzhechkov/memory": "0.2.9"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@ruvector/rvf": {
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Hold-out validated skill optimization (feature bto-optimize-holdout, ADR-001).
3
+ *
4
+ * Strengthens the EXISTING `/bto-optimize` skill from within with dspy-MIPROv2 rigor (grounded in the shipped
5
+ * `dspy.ts/src/optimize/miprov2.ts`: propose → minibatch-tune → validate on held-out → best). The current BTO
6
+ * loop selects the highest score on the SAME eval it tuned on — which games the LLM judge panel (Goodhart).
7
+ * This engine adds what it lacks: a deterministic tune/holdout split, a hard budget cap, a weakest-dimension
8
+ * objective, and a **no-regress-on-HOLD-OUT** winner selector. All functions are PURE over injected judge
9
+ * scores + text (no LLM, no clock, no random); candidate-prose generation and judging stay skill-side.
10
+ *
11
+ * SAFETY PROPERTIES (ADR-001, load-bearing, each pinned by a test):
12
+ * 1. §2 — `selectWinner` accepts a candidate ONLY on the HOLD-OUT scores (weakest dim improves + no other
13
+ * dim / the aggregate regresses beyond `tolerance`). A candidate that wins on `tune` but regresses on
14
+ * `holdout` is REJECTED — this defeats judge-gaming. `tune` never decides acceptance.
15
+ * 2. §? — `budgetPlan` NEVER returns a plan exceeding `maxJudgeRuns`: it shrinks candidates/rounds to fit
16
+ * and reports what it trimmed, so the loop can never run a surprise-cost number of judge passes.
17
+ * 3. §3 — the engine NEVER writes a file (only renders a diff + returns a decision); `proseScopeOk` rejects
18
+ * a candidate that touches frontmatter or structural headings (augment-not-clobber).
19
+ */
20
+
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+
23
+ export type BtoDimension = 'METHODOLOGY' | 'DEPTH' | 'CORRECTNESS' | 'USABILITY' | 'ROBUSTNESS';
24
+ export const BTO_DIMENSIONS: readonly BtoDimension[] = Object.freeze(['METHODOLOGY', 'DEPTH', 'CORRECTNESS', 'USABILITY', 'ROBUSTNESS']);
25
+ export type DimScores = Record<BtoDimension, number>;
26
+
27
+ const byStr = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
28
+
29
+ // ── FR-1: deterministic tune/holdout split ──────────────────────────────────────────────────────────────
30
+
31
+ export interface ScenarioSplit {
32
+ readonly tune: readonly string[];
33
+ readonly holdout: readonly string[];
34
+ }
35
+
36
+ /**
37
+ * Partition scenario ids into tune/holdout DETERMINISTICALLY (no clock/random): sort by id, then interleave —
38
+ * every k-th id (k = round(1/holdoutRatio)) goes to holdout. Guarantees ≥1 on each side when ≥2 ids exist.
39
+ * Same input → byte-identical split.
40
+ */
41
+ export function splitScenarios(ids: readonly string[], holdoutRatio = 0.34): ScenarioSplit {
42
+ const sorted = [...new Set(ids.map((s) => String(s)))].filter((s) => s !== '').sort(byStr);
43
+ if (sorted.length === 0) return { tune: [], holdout: [] };
44
+ if (sorted.length === 1) return { tune: sorted, holdout: [] };
45
+ const ratio = holdoutRatio > 0 && holdoutRatio < 1 ? holdoutRatio : 0.34;
46
+ const k = Math.max(2, Math.round(1 / ratio)); // every k-th → holdout
47
+ const tune: string[] = [];
48
+ const holdout: string[] = [];
49
+ sorted.forEach((id, i) => ((i + 1) % k === 0 ? holdout : tune).push(id));
50
+ // guarantee ≥1 each side
51
+ if (holdout.length === 0) holdout.push(tune.pop()!);
52
+ if (tune.length === 0) tune.push(holdout.pop()!);
53
+ return { tune, holdout };
54
+ }
55
+
56
+ // ── FR-2: budget plan with a hard cap ───────────────────────────────────────────────────────────────────
57
+
58
+ export interface BudgetInput {
59
+ readonly candidates: number;
60
+ readonly rounds: number;
61
+ readonly tuneCount: number;
62
+ readonly holdoutCount: number;
63
+ }
64
+ export interface BudgetPlan {
65
+ readonly candidates: number;
66
+ readonly rounds: number;
67
+ readonly tuneRuns: number;
68
+ readonly holdoutRuns: number;
69
+ readonly totalRuns: number;
70
+ readonly cap: number;
71
+ readonly withinCap: boolean;
72
+ readonly trimmed?: string;
73
+ }
74
+
75
+ /** Documented default cap (configurable — NOT a magic constant): a single L2 judge pass per scenario per
76
+ * candidate is the unit; 24 keeps a 5-candidate × ~4-scenario tune + holdout validation comfortably bounded. */
77
+ export const DEFAULT_MAX_JUDGE_RUNS = 24;
78
+
79
+ /**
80
+ * Judge-run accounting: tune scoring = candidates × rounds × tuneCount; holdout validation = 1 (top-1) ×
81
+ * holdoutCount. If the total exceeds `maxJudgeRuns`, SHRINK — first `rounds` to 1, then `candidates` — until it
82
+ * fits, and report what was trimmed. Never returns an over-cap plan (safety property 2).
83
+ */
84
+ export function budgetPlan(input: BudgetInput, maxJudgeRuns: number = DEFAULT_MAX_JUDGE_RUNS): BudgetPlan {
85
+ const cap = Number.isFinite(maxJudgeRuns) && maxJudgeRuns > 0 ? Math.floor(maxJudgeRuns) : DEFAULT_MAX_JUDGE_RUNS;
86
+ const tuneCount = Math.max(0, Math.floor(input.tuneCount));
87
+ const holdoutCount = Math.max(0, Math.floor(input.holdoutCount));
88
+ let candidates = Math.max(1, Math.floor(input.candidates));
89
+ let rounds = Math.max(1, Math.floor(input.rounds));
90
+ const trims: string[] = [];
91
+ const total = (c: number, r: number): number => c * r * tuneCount + holdoutCount;
92
+ if (total(candidates, rounds) > cap && rounds > 1) { rounds = 1; trims.push('rounds→1'); }
93
+ while (total(candidates, rounds) > cap && candidates > 1) { candidates -= 1; }
94
+ if (candidates < Math.max(1, Math.floor(input.candidates))) trims.push('candidates→' + candidates);
95
+ const tuneRuns = candidates * rounds * tuneCount;
96
+ const holdoutRuns = holdoutCount; // validate the top-1 candidate on holdout
97
+ const totalRuns = tuneRuns + holdoutRuns;
98
+ const plan: BudgetPlan = {
99
+ candidates, rounds, tuneRuns, holdoutRuns, totalRuns, cap, withinCap: totalRuns <= cap,
100
+ ...(trims.length ? { trimmed: trims.join(', ') } : {}),
101
+ };
102
+ return plan;
103
+ }
104
+
105
+ // ── FR-3 / aggregate ────────────────────────────────────────────────────────────────────────────────────
106
+
107
+ const dimVal = (s: DimScores, d: BtoDimension): number => {
108
+ const v = s[d];
109
+ return typeof v === 'number' && Number.isFinite(v) ? v : 0;
110
+ };
111
+
112
+ /** A judge score is a finite number in the rubric range [0, 10]. Anything else (missing, string, NaN,
113
+ * Infinity, negative, >10) is MALFORMED — it must never coerce silently to 0 and fabricate a winner (QE). */
114
+ const isValidScore = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 10;
115
+ /** Every one of the 5 BTO dimensions must be a valid score for a DimScores to be usable. */
116
+ export function validDimScores(s: unknown): s is DimScores {
117
+ if (s === null || typeof s !== 'object') return false;
118
+ const r = s as Record<string, unknown>;
119
+ return BTO_DIMENSIONS.every((d) => isValidScore(r[d]));
120
+ }
121
+ export function aggregate(s: DimScores): number {
122
+ return BTO_DIMENSIONS.reduce((sum, d) => sum + dimVal(s, d), 0) / BTO_DIMENSIONS.length;
123
+ }
124
+ /** The lowest-scoring dimension (the bottleneck to lift). Deterministic tie-break by BTO_DIMENSIONS order. */
125
+ export function weakestDimension(s: DimScores): BtoDimension {
126
+ let best: BtoDimension = BTO_DIMENSIONS[0]!;
127
+ for (const d of BTO_DIMENSIONS) if (dimVal(s, d) < dimVal(s, best)) best = d;
128
+ return best;
129
+ }
130
+
131
+ // ── FR-4: no-regress-on-HOLD-OUT winner selection (load-bearing) ─────────────────────────────────────────
132
+
133
+ export interface Candidate {
134
+ readonly id: string;
135
+ readonly prose: string;
136
+ readonly tune: DimScores;
137
+ readonly holdout: DimScores;
138
+ }
139
+ export interface WinnerResult {
140
+ readonly winner: string | null;
141
+ readonly reason: string;
142
+ readonly weakest: BtoDimension;
143
+ readonly deltas?: Record<string, number>;
144
+ }
145
+
146
+ /**
147
+ * Accept a candidate ONLY on the HOLD-OUT scores (ADR §2): the baseline's weakest dimension must improve by > 0
148
+ * AND no other dimension AND the aggregate may regress by more than `tolerance` (default 0 = strict). Candidates
149
+ * are considered in descending TUNE-aggregate order (the minibatch rank — dspy's search), but acceptance reads
150
+ * only holdout, so a tune-winner that regresses on holdout is rejected. Deterministic; returns the first
151
+ * candidate that passes, or null with a reason.
152
+ */
153
+ export function selectWinner(baseline: { readonly holdout: DimScores }, candidates: readonly Candidate[], opts: { tolerance?: number } = {}): WinnerResult {
154
+ // Reject malformed input BEFORE any comparison (QE): coercing missing/junk dims to 0 fabricates a winner.
155
+ if (!baseline || !validDimScores(baseline.holdout)) {
156
+ return { winner: null, reason: 'invalid baseline holdout scores (every dimension must be a number in [0,10])', weakest: BTO_DIMENSIONS[0]! };
157
+ }
158
+ // tolerance must itself be a finite non-negative number (Infinity would disable the no-regress guard).
159
+ const tolerance = typeof opts.tolerance === 'number' && Number.isFinite(opts.tolerance) && opts.tolerance >= 0 ? opts.tolerance : 0;
160
+ const weakest = weakestDimension(baseline.holdout);
161
+ // only candidates with VALID holdout scores are eligible; a malformed candidate can never win.
162
+ const eligible = candidates.filter((c) => validDimScores(c.holdout) && validDimScores(c.tune));
163
+ const ranked = [...eligible].sort((a, b) => aggregate(b.tune) - aggregate(a.tune) || byStr(a.id, b.id));
164
+ let lastReason = `no candidate improved the weakest dimension (${weakest}) on the holdout without regression`;
165
+ for (const c of ranked) {
166
+ const weakDelta = dimVal(c.holdout, weakest) - dimVal(baseline.holdout, weakest);
167
+ if (weakDelta <= 0) { lastReason = `${c.id}: weakest ${weakest} did not improve on holdout (Δ=${weakDelta.toFixed(2)})`; continue; }
168
+ // no-regress on every OTHER dimension + the aggregate
169
+ let regressed: string | null = null;
170
+ for (const d of BTO_DIMENSIONS) {
171
+ if (d === weakest) continue;
172
+ const delta = dimVal(c.holdout, d) - dimVal(baseline.holdout, d);
173
+ if (delta < -tolerance) { regressed = `${d} (Δ=${delta.toFixed(2)})`; break; }
174
+ }
175
+ const aggDelta = aggregate(c.holdout) - aggregate(baseline.holdout);
176
+ if (!regressed && aggDelta < -tolerance) regressed = `aggregate (Δ=${aggDelta.toFixed(2)})`;
177
+ if (regressed) { lastReason = `${c.id}: lifted ${weakest} but regressed ${regressed} on holdout`; continue; }
178
+ return {
179
+ winner: c.id,
180
+ reason: `accepted on holdout: ${weakest} +${weakDelta.toFixed(2)}, aggregate ${aggDelta >= 0 ? '+' : ''}${aggDelta.toFixed(2)}, no regression`,
181
+ weakest,
182
+ deltas: { [weakest]: weakDelta, aggregate: aggDelta },
183
+ };
184
+ }
185
+ return { winner: null, reason: lastReason, weakest };
186
+ }
187
+
188
+ // ── FR-7: prose-scope guard + diff ──────────────────────────────────────────────────────────────────────
189
+
190
+ // Normalize CRLF/CR → LF first, else a `\r\n` file evades the `\n`-anchored frontmatter/heading regexes (QE bypass).
191
+ const normEol = (t: string): string => t.replace(/\r\n?/g, '\n');
192
+ const frontmatter = (t: string): string => {
193
+ const m = /^---\n([\s\S]*?)\n---/.exec(normEol(t));
194
+ return m ? m[1]! : '';
195
+ };
196
+ /** The document BODY (after the frontmatter block) — so the frontmatter's own closing `---` is never misread
197
+ * as a setext underline, and heading detection runs only on prose. */
198
+ const bodyAfterFrontmatter = (t: string): string => {
199
+ const n = normEol(t);
200
+ const m = /^---\n[\s\S]*?\n---\n?/.exec(n);
201
+ return m ? n.slice(m[0].length) : n;
202
+ };
203
+
204
+ /**
205
+ * ALL structural markers on the BODY, not just space-delimited ATX (QE: `##\tNEW`, bare `##`, and setext
206
+ * `Title\n===` / `Title\n---` evaded the old regex). Fenced code blocks (``` / ~~~) are SKIPPED so a `===`/`---`
207
+ * line INSIDE code is not misread as a heading (QE false-positive). Collect ATX (`#`..`######` + any/no ws)
208
+ * AND setext underlines (a non-empty line immediately followed by `=+`/`-+`).
209
+ */
210
+ const headings = (t: string): string[] => {
211
+ const lines = bodyAfterFrontmatter(t).split('\n');
212
+ const out: string[] = [];
213
+ let inFence = false;
214
+ for (let i = 0; i < lines.length; i++) {
215
+ const line = lines[i]!;
216
+ if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; continue; }
217
+ if (inFence) continue;
218
+ const atx = /^(#{1,6})(?:\s.*)?$/.exec(line.replace(/\s+$/, ''));
219
+ if (atx) { out.push('ATX:' + line.trim()); continue; }
220
+ const next = lines[i + 1];
221
+ if (line.trim() !== '' && !/^\s*(```|~~~)/.test(next ?? '') && next !== undefined && /^(=+|-+)\s*$/.test(next)) {
222
+ out.push('SETEXT:' + line.trim() + '|' + next.trim());
223
+ }
224
+ }
225
+ return out;
226
+ };
227
+
228
+ /** Only directive PROSE may change (Phase-1): frontmatter + the set of ALL markdown structural markers must be
229
+ * identical. A candidate that alters them (incl. CRLF, tab-ATX, empty-ATX, setext) is rejected (augment-not-clobber). */
230
+ export function proseScopeOk(original: string, candidate: string): { ok: boolean; reason: string } {
231
+ if (frontmatter(original) !== frontmatter(candidate)) return { ok: false, reason: 'candidate changed the YAML frontmatter (out of scope: prose only)' };
232
+ const ho = headings(original);
233
+ const hc = headings(candidate);
234
+ if (ho.length !== hc.length || ho.some((h, i) => h !== hc[i])) return { ok: false, reason: 'candidate changed the section headings/structure (out of scope: prose only)' };
235
+ return { ok: true, reason: 'prose-only change' };
236
+ }
237
+
238
+ /** A minimal deterministic unified-ish line diff for the confirm gate (pure). */
239
+ export function renderProseDiff(original: string, candidate: string): string {
240
+ const a = original.split('\n');
241
+ const b = candidate.split('\n');
242
+ const out: string[] = ['--- current', '+++ candidate'];
243
+ const max = Math.max(a.length, b.length);
244
+ for (let i = 0; i < max; i++) {
245
+ const l = a[i];
246
+ const r = b[i];
247
+ if (l === r) continue;
248
+ if (l !== undefined) out.push('- ' + l);
249
+ if (r !== undefined) out.push('+ ' + r);
250
+ }
251
+ return out.length > 2 ? out.join('\n') : '(no textual difference)';
252
+ }
253
+
254
+ // ── thin I/O (top-level ESM fs; never throws) ───────────────────────────────────────────────────────────
255
+
256
+ /** Read a scenario-id list: JSON array, or newline/comma-separated. Absent/unreadable → []. */
257
+ export function readScenarioIds(path: string): string[] {
258
+ try {
259
+ if (!existsSync(path)) return [];
260
+ const raw = readFileSync(path, 'utf8').trim();
261
+ if (raw === '') return [];
262
+ if (raw.startsWith('[')) {
263
+ const arr = JSON.parse(raw) as unknown;
264
+ return Array.isArray(arr) ? arr.map((x) => String(x)).filter((s) => s !== '') : [];
265
+ }
266
+ return raw.split(/[\n,]/).map((s) => s.trim()).filter((s) => s !== '');
267
+ } catch {
268
+ return [];
269
+ }
270
+ }
@@ -220,6 +220,17 @@ export const DEFAULT_MODELS: Record<string, string | null> = {
220
220
  * - `'fable'|'opus'|…` → `{model}`
221
221
  * - unknown → warn + `{}` (Claude) / `CODEX_MODEL` (codex id)
222
222
  */
223
+ /**
224
+ * The learned cost-optimal routing spec (feature learned-cost-routing). A stage whose spec is `'auto-cost'`
225
+ * is resolved by the WORKFLOW against the live outcome store + probed ladder (I/O there; pure selection in
226
+ * `routing-outcomes.ts`), NOT here — so `specToOpts`/`resolveStageModel` stay byte-identical when it is absent.
227
+ * `specToOpts('auto-cost')` deliberately degrades to session-inherited if it ever leaks through.
228
+ */
229
+ export const AUTO_COST_SPEC = 'auto-cost';
230
+ export function isAutoCostSpec(spec: string | null | undefined): boolean {
231
+ return spec === AUTO_COST_SPEC;
232
+ }
233
+
223
234
  export function specToOpts(spec: string | null | undefined, env: RoutingEnv): StageOpts {
224
235
  const log = env.log || function () {};
225
236
  if (!spec) return {};
package/src/index.ts CHANGED
@@ -269,3 +269,5 @@ export * from './rake-analyzer.js';
269
269
  export * from './session-retro.js';
270
270
  export * from './feature-adr-setup.js';
271
271
  export * from './challenge-panel.js';
272
+ export * from './routing-outcomes.js';
273
+ export * from './bto-optimize.js';