@deftai/directive-core 0.69.0 → 0.71.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.
- package/dist/branch/evaluate.js +7 -0
- package/dist/deposit/contain.d.ts +41 -0
- package/dist/deposit/contain.js +113 -0
- package/dist/deposit/copy-tree.js +8 -2
- package/dist/doctor/checks.d.ts +11 -0
- package/dist/doctor/checks.js +75 -2
- package/dist/doctor/doctor-state.js +2 -2
- package/dist/doctor/main.js +54 -1
- package/dist/doctor/manifest.d.ts +20 -0
- package/dist/doctor/manifest.js +22 -0
- package/dist/doctor/types.d.ts +8 -0
- package/dist/eval/crud-telemetry.d.ts +59 -0
- package/dist/eval/crud-telemetry.js +307 -0
- package/dist/eval/health.d.ts +52 -0
- package/dist/eval/health.js +240 -0
- package/dist/eval/readback.d.ts +38 -0
- package/dist/eval/readback.js +209 -0
- package/dist/eval/report.d.ts +53 -0
- package/dist/eval/report.js +161 -0
- package/dist/eval/run.d.ts +79 -0
- package/dist/eval/run.js +309 -0
- package/dist/events/attribution-constants.d.ts +13 -0
- package/dist/events/attribution-constants.js +18 -0
- package/dist/events/attribution-ledger.d.ts +43 -0
- package/dist/events/attribution-ledger.js +61 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/gitignore.js +5 -5
- package/dist/init-deposit/init-deposit.js +6 -0
- package/dist/init-deposit/refresh.js +5 -0
- package/dist/intake/issue-ingest.d.ts +11 -0
- package/dist/intake/issue-ingest.js +86 -11
- package/dist/layout/resolve.d.ts +2 -2
- package/dist/layout/resolve.js +2 -2
- package/dist/lifecycle/events.js +2 -0
- package/dist/policy/index.d.ts +2 -1
- package/dist/policy/index.js +15 -2
- package/dist/policy/plan-extensions.d.ts +31 -0
- package/dist/policy/plan-extensions.js +45 -0
- package/dist/policy/value-feedback.d.ts +56 -0
- package/dist/policy/value-feedback.js +284 -0
- package/dist/policy/wip.d.ts +2 -2
- package/dist/policy/wip.js +2 -2
- package/dist/preflight-cache/evaluate.js +4 -3
- package/dist/scope/audit-log.js +3 -3
- package/dist/scope/transition.js +16 -5
- package/dist/session/session-start.js +20 -0
- package/dist/slice/record.js +3 -3
- package/dist/swarm/routing.js +10 -3
- package/dist/triage/actions/candidates-log.d.ts +4 -4
- package/dist/triage/actions/candidates-log.js +4 -7
- package/dist/triage/bootstrap/gitignore.d.ts +7 -3
- package/dist/triage/bootstrap/gitignore.js +67 -37
- package/dist/triage/bootstrap/index.d.ts +1 -1
- package/dist/triage/bootstrap/index.js +4 -3
- package/dist/triage/bulk/index.d.ts +11 -2
- package/dist/triage/bulk/index.js +43 -6
- package/dist/triage/cache-path.d.ts +39 -0
- package/dist/triage/cache-path.js +98 -0
- package/dist/triage/help/registry-data.d.ts +53 -20
- package/dist/triage/help/registry-data.js +104 -20
- package/dist/triage/index.d.ts +2 -0
- package/dist/triage/index.js +2 -0
- package/dist/triage/queue/audit.js +2 -2
- package/dist/triage/queue/cache.js +2 -2
- package/dist/triage/reconcile/reconcile.js +4 -3
- package/dist/triage/scope/cli.d.ts +2 -1
- package/dist/triage/scope/cli.js +30 -6
- package/dist/triage/scope/mutations-core.js +3 -2
- package/dist/triage/scope/mutations.d.ts +10 -0
- package/dist/triage/scope/mutations.js +22 -0
- package/dist/triage/subscribe/index.d.ts +1 -1
- package/dist/triage/subscribe/index.js +4 -3
- package/dist/triage/summary/index.d.ts +2 -2
- package/dist/triage/summary/index.js +4 -3
- package/dist/triage/summary/reconcilable.d.ts +1 -1
- package/dist/triage/summary/reconcilable.js +3 -2
- package/dist/triage/welcome/constants.d.ts +3 -4
- package/dist/triage/welcome/constants.js +3 -4
- package/dist/triage/welcome/index.d.ts +1 -0
- package/dist/triage/welcome/index.js +1 -0
- package/dist/triage/welcome/onboard.d.ts +35 -0
- package/dist/triage/welcome/onboard.js +94 -0
- package/dist/triage/welcome/prior-state.js +3 -2
- package/dist/triage/welcome/summary.js +5 -4
- package/dist/umbrella-current-shape/index.d.ts +25 -1
- package/dist/umbrella-current-shape/index.js +84 -4
- package/dist/value/adoption-emit.d.ts +14 -0
- package/dist/value/adoption-emit.js +67 -0
- package/dist/value/adoption-registry.d.ts +62 -0
- package/dist/value/adoption-registry.js +214 -0
- package/dist/value/feedback-file.d.ts +67 -0
- package/dist/value/feedback-file.js +353 -0
- package/dist/value/friction-emit.d.ts +10 -0
- package/dist/value/friction-emit.js +24 -0
- package/dist/value/readback.d.ts +86 -0
- package/dist/value/readback.js +503 -0
- package/dist/vbrief-build/project-definition-io.d.ts +6 -0
- package/dist/vbrief-build/project-definition-io.js +7 -2
- package/dist/vbrief-validate/decomposition.js +1 -1
- package/dist/vbrief-validate/precutover.js +7 -3
- package/dist/wip-cap/evaluate.js +3 -0
- package/package.json +35 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { MAX_LINE_CHARS } from "../triage/welcome/constants.js";
|
|
4
|
+
import { evaluateHealth, healthHistoryPath } from "./health.js";
|
|
5
|
+
/** Repeat-suppression window for the budgeted eval session nudge (#1703 / #1279 parity). */
|
|
6
|
+
export const EVAL_READBACK_SUPPRESSION_HOURS = 4;
|
|
7
|
+
export const EVAL_READBACK_HISTORY_REL = join(".deft-cache", "eval-readback-history.jsonl");
|
|
8
|
+
export const EVAL_READBACK_HISTORY_SCHEMA = "deft.eval.readback.v1";
|
|
9
|
+
function truncate(text, maxChars) {
|
|
10
|
+
if (text.length <= maxChars) {
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
if (maxChars <= 3) {
|
|
14
|
+
return text.slice(0, maxChars);
|
|
15
|
+
}
|
|
16
|
+
return `${text.slice(0, maxChars - 3)}...`;
|
|
17
|
+
}
|
|
18
|
+
function parseJsonObjectOrNull(text) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(text);
|
|
21
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// malformed history line
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function readHealthHistoryRows(projectRoot) {
|
|
31
|
+
const path = healthHistoryPath(projectRoot);
|
|
32
|
+
if (!existsSync(path)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
let text;
|
|
36
|
+
try {
|
|
37
|
+
text = readFileSync(path, "utf8");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const rows = [];
|
|
43
|
+
for (const line of text.split("\n")) {
|
|
44
|
+
const stripped = line.trim();
|
|
45
|
+
if (stripped.length === 0) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
rows.push(JSON.parse(stripped));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// skip malformed ledger rows
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
57
|
+
function readbackHistoryPath(projectRoot) {
|
|
58
|
+
return resolve(projectRoot, EVAL_READBACK_HISTORY_REL);
|
|
59
|
+
}
|
|
60
|
+
function readLastReadbackRecord(path) {
|
|
61
|
+
if (!existsSync(path)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
text = readFileSync(path, "utf8");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const lines = text.split("\n").filter((line) => line.trim().length > 0);
|
|
72
|
+
if (lines.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const last = lines[lines.length - 1];
|
|
76
|
+
if (last === undefined) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return parseJsonObjectOrNull(last);
|
|
80
|
+
}
|
|
81
|
+
function parseHistoryEmittedAt(record) {
|
|
82
|
+
const raw = record.emitted_at;
|
|
83
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
let candidate = raw.trim();
|
|
87
|
+
if (candidate.endsWith("Z")) {
|
|
88
|
+
candidate = `${candidate.slice(0, -1)}+00:00`;
|
|
89
|
+
}
|
|
90
|
+
const parsed = new Date(candidate);
|
|
91
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
92
|
+
}
|
|
93
|
+
/** Stable nudge key for debounce — score + contradiction ids. */
|
|
94
|
+
export function evalReadbackNudgeKey(report) {
|
|
95
|
+
const contradictionIds = report.contradictions
|
|
96
|
+
.map((c) => c.id)
|
|
97
|
+
.sort()
|
|
98
|
+
.join(",");
|
|
99
|
+
return `eval-health:${report.score}:${contradictionIds}`;
|
|
100
|
+
}
|
|
101
|
+
/** True when the same eval nudge was read back within the suppression window. */
|
|
102
|
+
export function shouldSuppressEvalReadback(nudgeKey, historyFile, options = {}) {
|
|
103
|
+
const prior = readLastReadbackRecord(historyFile);
|
|
104
|
+
if (prior === null) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const emittedAt = parseHistoryEmittedAt(prior);
|
|
108
|
+
if (emittedAt === null) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const now = options.now ?? new Date();
|
|
112
|
+
const ageMs = now.getTime() - emittedAt.getTime();
|
|
113
|
+
if (ageMs < 0 || ageMs >= EVAL_READBACK_SUPPRESSION_HOURS * 3_600_000) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return prior.nudge_key === nudgeKey;
|
|
117
|
+
}
|
|
118
|
+
function appendEvalReadbackHistory(projectRoot, nudgeKey, line, options = {}) {
|
|
119
|
+
const path = readbackHistoryPath(projectRoot);
|
|
120
|
+
const record = {
|
|
121
|
+
schema: EVAL_READBACK_HISTORY_SCHEMA,
|
|
122
|
+
emitted_at: (options.now ?? new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
123
|
+
nudge_key: nudgeKey,
|
|
124
|
+
line,
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
128
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// observability only
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function failedGateCount(report) {
|
|
135
|
+
return report.gates.filter((g) => !g.skipped && !g.pass).length;
|
|
136
|
+
}
|
|
137
|
+
/** Whether the current health report warrants a session-start advisory nudge (#2336). */
|
|
138
|
+
export function shouldNudgeEvalHealth(report, previous) {
|
|
139
|
+
if (report.contradictions.length > 0) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (previous !== null && report.score < previous.score) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (previous === null && report.score < 100 && failedGateCount(report) > 0) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
export function formatEvalHealthSessionLine(report, previous) {
|
|
151
|
+
if (report.contradictions.length > 0) {
|
|
152
|
+
const first = report.contradictions[0];
|
|
153
|
+
const summary = first?.summary ?? "contradictory gate detected";
|
|
154
|
+
return (`[eval] Framework health ${report.score}/100 — contradictory gate ${first?.id ?? "unknown"}: ` +
|
|
155
|
+
`${summary.replace(/\r?\n/g, " ")} — run \`task eval:health\` for detail.`);
|
|
156
|
+
}
|
|
157
|
+
if (previous !== null && report.score < previous.score) {
|
|
158
|
+
return (`[eval] Framework health dropped ${previous.score}->${report.score}/100 — ` +
|
|
159
|
+
"run `task eval:health` for gate detail.");
|
|
160
|
+
}
|
|
161
|
+
const failed = failedGateCount(report);
|
|
162
|
+
return (`[eval] Framework health ${report.score}/100 (${failed} gate${failed === 1 ? "" : "s"} failing) — ` +
|
|
163
|
+
"run `task eval:health` for detail.");
|
|
164
|
+
}
|
|
165
|
+
/** Budgeted session eval one-liner — silent when healthy or debounced (#1703 / #2336). */
|
|
166
|
+
export function renderSessionEvalReadback(projectRoot, options = {}) {
|
|
167
|
+
try {
|
|
168
|
+
const root = resolve(projectRoot);
|
|
169
|
+
const priorRows = readHealthHistoryRows(root);
|
|
170
|
+
const previous = priorRows.length > 0 ? (priorRows[priorRows.length - 1] ?? null) : null;
|
|
171
|
+
const evaluateFn = options.evaluate ??
|
|
172
|
+
((cwd) => {
|
|
173
|
+
const result = evaluateHealth({ projectRoot: cwd, persist: true });
|
|
174
|
+
return { report: result.report };
|
|
175
|
+
});
|
|
176
|
+
const { report } = evaluateFn(root);
|
|
177
|
+
if (report === null) {
|
|
178
|
+
return { line: null, suppressed: false };
|
|
179
|
+
}
|
|
180
|
+
if (!shouldNudgeEvalHealth(report, previous)) {
|
|
181
|
+
return { line: null, suppressed: false };
|
|
182
|
+
}
|
|
183
|
+
const nudgeKey = evalReadbackNudgeKey(report);
|
|
184
|
+
const hist = readbackHistoryPath(root);
|
|
185
|
+
if (shouldSuppressEvalReadback(nudgeKey, hist, { now: options.now })) {
|
|
186
|
+
return { line: null, suppressed: true };
|
|
187
|
+
}
|
|
188
|
+
const maxChars = options.maxChars ?? MAX_LINE_CHARS;
|
|
189
|
+
const line = truncate(formatEvalHealthSessionLine(report, previous), maxChars);
|
|
190
|
+
if (options.writeHistory !== false) {
|
|
191
|
+
appendEvalReadbackHistory(root, nudgeKey, line, { now: options.now });
|
|
192
|
+
}
|
|
193
|
+
return { line, suppressed: false };
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return { line: null, suppressed: false };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Emit the eval session readback line when present. Returns the line or null. */
|
|
200
|
+
export function emitSessionEvalReadback(projectRoot, options = {}) {
|
|
201
|
+
const result = renderSessionEvalReadback(projectRoot, options);
|
|
202
|
+
if (result.line === null) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const out = options.output ?? ((line) => process.stdout.write(`${line}\n`));
|
|
206
|
+
out(result.line);
|
|
207
|
+
return result.line;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=readback.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type GoldenRunRecord } from "./run.js";
|
|
2
|
+
export declare const REPORT_SCHEMA_VERSION: 1;
|
|
3
|
+
/** Metric delta with a simple two-proportion significance test (#896). */
|
|
4
|
+
export interface MetricDelta {
|
|
5
|
+
readonly metric: string;
|
|
6
|
+
readonly champion: number;
|
|
7
|
+
readonly challenger: number;
|
|
8
|
+
readonly delta: number;
|
|
9
|
+
readonly zScore: number | null;
|
|
10
|
+
readonly pValue: number | null;
|
|
11
|
+
readonly significantAt05: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Holdout tripwire when primary gains do not generalize (#1703 Goodhart guard). */
|
|
14
|
+
export interface HoldoutTripwire {
|
|
15
|
+
readonly triggered: boolean;
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
readonly primaryDelta: number;
|
|
18
|
+
readonly holdoutDelta: number;
|
|
19
|
+
}
|
|
20
|
+
/** Version-diff report between champion and challenger golden runs. */
|
|
21
|
+
export interface GoldenEvalReport {
|
|
22
|
+
readonly schemaVersion: typeof REPORT_SCHEMA_VERSION;
|
|
23
|
+
readonly championVersion: string;
|
|
24
|
+
readonly challengerVersion: string;
|
|
25
|
+
readonly model: string;
|
|
26
|
+
readonly championRunId: string;
|
|
27
|
+
readonly challengerRunId: string;
|
|
28
|
+
readonly deltas: readonly MetricDelta[];
|
|
29
|
+
readonly holdoutTripwire: HoldoutTripwire;
|
|
30
|
+
}
|
|
31
|
+
export interface ReportGoldenEvalOptions {
|
|
32
|
+
readonly projectRoot?: string;
|
|
33
|
+
readonly championVersion: string;
|
|
34
|
+
readonly challengerVersion: string;
|
|
35
|
+
readonly model: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ReportGoldenEvalResult {
|
|
38
|
+
readonly code: 0 | 1 | 2;
|
|
39
|
+
readonly report: GoldenEvalReport | null;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}
|
|
42
|
+
/** Pick the latest run for a directive version and model. */
|
|
43
|
+
export declare function findLatestGoldenRun(records: readonly GoldenRunRecord[], directiveVersion: string, model: string): GoldenRunRecord | null;
|
|
44
|
+
/** Two-proportion z-test (normal approximation). */
|
|
45
|
+
export declare function twoProportionZTest(passedA: number, totalA: number, passedB: number, totalB: number): {
|
|
46
|
+
zScore: number | null;
|
|
47
|
+
pValue: number | null;
|
|
48
|
+
};
|
|
49
|
+
/** Detect holdout tripwire: primary improves while holdout regresses (#1703). */
|
|
50
|
+
export declare function evaluateHoldoutTripwire(champion: GoldenRunRecord, challenger: GoldenRunRecord): HoldoutTripwire;
|
|
51
|
+
/** Diff two directive versions with metric deltas and significance (#1703 Tier 2). */
|
|
52
|
+
export declare function reportGoldenEval(options: ReportGoldenEvalOptions): ReportGoldenEvalResult;
|
|
53
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolveEvalPath } from "../layout/resolve.js";
|
|
3
|
+
import { GOLDEN_RUNS_HISTORY_REL } from "./run.js";
|
|
4
|
+
export const REPORT_SCHEMA_VERSION = 1;
|
|
5
|
+
function readGoldenRuns(projectRoot) {
|
|
6
|
+
const path = resolveEvalPath(projectRoot, GOLDEN_RUNS_HISTORY_REL);
|
|
7
|
+
if (!existsSync(path)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const lines = readFileSync(path, "utf8")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.filter((line) => line.trim().length > 0);
|
|
13
|
+
const records = [];
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
try {
|
|
16
|
+
records.push(JSON.parse(line));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Skip malformed ledger rows.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return records;
|
|
23
|
+
}
|
|
24
|
+
/** Pick the latest run for a directive version and model. */
|
|
25
|
+
export function findLatestGoldenRun(records, directiveVersion, model) {
|
|
26
|
+
const matches = records.filter((r) => r.directiveVersion === directiveVersion && r.model === model);
|
|
27
|
+
if (matches.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return matches.reduce((latest, current) => current.recordedAt >= latest.recordedAt ? current : latest);
|
|
31
|
+
}
|
|
32
|
+
function countPasses(results) {
|
|
33
|
+
return {
|
|
34
|
+
passed: results.filter((r) => r.passed).length,
|
|
35
|
+
total: results.length,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Two-proportion z-test (normal approximation). */
|
|
39
|
+
export function twoProportionZTest(passedA, totalA, passedB, totalB) {
|
|
40
|
+
if (totalA === 0 || totalB === 0) {
|
|
41
|
+
return { zScore: null, pValue: null };
|
|
42
|
+
}
|
|
43
|
+
const p1 = passedA / totalA;
|
|
44
|
+
const p2 = passedB / totalB;
|
|
45
|
+
const pooled = (passedA + passedB) / (totalA + totalB);
|
|
46
|
+
const se = Math.sqrt(pooled * (1 - pooled) * (1 / totalA + 1 / totalB));
|
|
47
|
+
if (se === 0) {
|
|
48
|
+
return { zScore: null, pValue: null };
|
|
49
|
+
}
|
|
50
|
+
const z = (p2 - p1) / se;
|
|
51
|
+
const pValue = 2 * (1 - normalCdf(Math.abs(z)));
|
|
52
|
+
return { zScore: z, pValue };
|
|
53
|
+
}
|
|
54
|
+
function normalCdf(x) {
|
|
55
|
+
return 0.5 * (1 + erf(x / Math.SQRT2));
|
|
56
|
+
}
|
|
57
|
+
function erf(x) {
|
|
58
|
+
const sign = x < 0 ? -1 : 1;
|
|
59
|
+
const ax = Math.abs(x);
|
|
60
|
+
const t = 1 / (1 + 0.3275911 * ax);
|
|
61
|
+
const y = 1 -
|
|
62
|
+
((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) *
|
|
63
|
+
t *
|
|
64
|
+
Math.exp(-ax * ax);
|
|
65
|
+
return sign * y;
|
|
66
|
+
}
|
|
67
|
+
function metricDelta(metric, championRate, challengerRate, championPassed, championTotal, challengerPassed, challengerTotal) {
|
|
68
|
+
const { zScore, pValue } = twoProportionZTest(championPassed, championTotal, challengerPassed, challengerTotal);
|
|
69
|
+
return {
|
|
70
|
+
metric,
|
|
71
|
+
champion: championRate,
|
|
72
|
+
challenger: challengerRate,
|
|
73
|
+
delta: challengerRate - championRate,
|
|
74
|
+
zScore,
|
|
75
|
+
pValue,
|
|
76
|
+
significantAt05: pValue !== null && pValue < 0.05,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Detect holdout tripwire: primary improves while holdout regresses (#1703). */
|
|
80
|
+
export function evaluateHoldoutTripwire(champion, challenger) {
|
|
81
|
+
const primaryDelta = challenger.summary.primaryPassRate - champion.summary.primaryPassRate;
|
|
82
|
+
const holdoutDelta = challenger.summary.holdoutPassRate - champion.summary.holdoutPassRate;
|
|
83
|
+
const triggered = primaryDelta > 0.05 && holdoutDelta < -0.05;
|
|
84
|
+
const summary = triggered
|
|
85
|
+
? "Holdout tripwire: challenger improved primary metrics but holdout regressed -- possible tuning to gated corpus."
|
|
86
|
+
: "Holdout tripwire clear.";
|
|
87
|
+
return { triggered, summary, primaryDelta, holdoutDelta };
|
|
88
|
+
}
|
|
89
|
+
function formatPercent(value) {
|
|
90
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
91
|
+
}
|
|
92
|
+
/** Diff two directive versions with metric deltas and significance (#1703 Tier 2). */
|
|
93
|
+
export function reportGoldenEval(options) {
|
|
94
|
+
if (!options.championVersion.trim() || !options.challengerVersion.trim()) {
|
|
95
|
+
return {
|
|
96
|
+
code: 2,
|
|
97
|
+
report: null,
|
|
98
|
+
message: "eval:report: --champion and --challenger are required",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!options.model.trim()) {
|
|
102
|
+
return { code: 2, report: null, message: "eval:report: --model is required" };
|
|
103
|
+
}
|
|
104
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
105
|
+
const records = readGoldenRuns(projectRoot);
|
|
106
|
+
const champion = findLatestGoldenRun(records, options.championVersion, options.model);
|
|
107
|
+
const challenger = findLatestGoldenRun(records, options.challengerVersion, options.model);
|
|
108
|
+
if (champion === null) {
|
|
109
|
+
return {
|
|
110
|
+
code: 1,
|
|
111
|
+
report: null,
|
|
112
|
+
message: `eval:report: no golden run found for champion v${options.championVersion} model=${options.model}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (challenger === null) {
|
|
116
|
+
return {
|
|
117
|
+
code: 1,
|
|
118
|
+
report: null,
|
|
119
|
+
message: `eval:report: no golden run found for challenger v${options.challengerVersion} model=${options.model}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const championPrimary = champion.results.filter((r) => !r.holdout);
|
|
123
|
+
const challengerPrimary = challenger.results.filter((r) => !r.holdout);
|
|
124
|
+
const championHoldout = champion.results.filter((r) => r.holdout);
|
|
125
|
+
const challengerHoldout = challenger.results.filter((r) => r.holdout);
|
|
126
|
+
const championPrimaryCount = countPasses(championPrimary);
|
|
127
|
+
const challengerPrimaryCount = countPasses(challengerPrimary);
|
|
128
|
+
const championHoldoutCount = countPasses(championHoldout);
|
|
129
|
+
const challengerHoldoutCount = countPasses(challengerHoldout);
|
|
130
|
+
const deltas = [
|
|
131
|
+
metricDelta("primaryPassRate", champion.summary.primaryPassRate, challenger.summary.primaryPassRate, championPrimaryCount.passed, championPrimaryCount.total, challengerPrimaryCount.passed, challengerPrimaryCount.total),
|
|
132
|
+
metricDelta("holdoutPassRate", champion.summary.holdoutPassRate, challenger.summary.holdoutPassRate, championHoldoutCount.passed, championHoldoutCount.total, challengerHoldoutCount.passed, challengerHoldoutCount.total),
|
|
133
|
+
metricDelta("overallPassRate", champion.summary.passRate, challenger.summary.passRate, countPasses(champion.results).passed, countPasses(champion.results).total, countPasses(challenger.results).passed, countPasses(challenger.results).total),
|
|
134
|
+
];
|
|
135
|
+
const holdoutTripwire = evaluateHoldoutTripwire(champion, challenger);
|
|
136
|
+
const report = {
|
|
137
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
138
|
+
championVersion: options.championVersion,
|
|
139
|
+
challengerVersion: options.challengerVersion,
|
|
140
|
+
model: options.model,
|
|
141
|
+
championRunId: champion.runId,
|
|
142
|
+
challengerRunId: challenger.runId,
|
|
143
|
+
deltas,
|
|
144
|
+
holdoutTripwire,
|
|
145
|
+
};
|
|
146
|
+
const lines = [
|
|
147
|
+
`eval:report champion=v${options.championVersion} challenger=v${options.challengerVersion} model=${options.model}`,
|
|
148
|
+
...deltas.map((d) => {
|
|
149
|
+
const sig = d.pValue === null
|
|
150
|
+
? "n/a"
|
|
151
|
+
: d.significantAt05
|
|
152
|
+
? `p=${d.pValue.toFixed(4)} *`
|
|
153
|
+
: `p=${d.pValue.toFixed(4)}`;
|
|
154
|
+
return ` ${d.metric}: ${formatPercent(d.champion)} -> ${formatPercent(d.challenger)} (delta ${(d.delta * 100).toFixed(1)}pp, ${sig})`;
|
|
155
|
+
}),
|
|
156
|
+
` ${holdoutTripwire.summary}`,
|
|
157
|
+
];
|
|
158
|
+
const code = holdoutTripwire.triggered ? 1 : 0;
|
|
159
|
+
return { code, report, message: lines.join("\n") };
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare const GOLDEN_RUNS_HISTORY_REL = "results/golden-runs.jsonl";
|
|
2
|
+
export declare const GOLDEN_RUN_SCHEMA_VERSION: 1;
|
|
3
|
+
export declare const GOLDEN_CORPUS_VERSION = "2026-07-05-tier2-fixture-v1";
|
|
4
|
+
/** One synthetic golden-task definition with an objective grader. */
|
|
5
|
+
export interface GoldenTaskDefinition {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly holdout: boolean;
|
|
9
|
+
readonly grade: (context: GoldenTaskContext) => GoldenTaskGrade;
|
|
10
|
+
}
|
|
11
|
+
/** Runtime context passed to golden-task graders. */
|
|
12
|
+
export interface GoldenTaskContext {
|
|
13
|
+
readonly tempDir: string;
|
|
14
|
+
readonly seed: number;
|
|
15
|
+
readonly directiveVersion: string;
|
|
16
|
+
readonly model: string;
|
|
17
|
+
}
|
|
18
|
+
/** Objective grader output for a single task × seed. */
|
|
19
|
+
export interface GoldenTaskGrade {
|
|
20
|
+
readonly passed: boolean;
|
|
21
|
+
readonly metrics: Readonly<Record<string, number | boolean>>;
|
|
22
|
+
}
|
|
23
|
+
/** One graded task outcome within a golden run. */
|
|
24
|
+
export interface GoldenTaskResult {
|
|
25
|
+
readonly taskId: string;
|
|
26
|
+
readonly seed: number;
|
|
27
|
+
readonly passed: boolean;
|
|
28
|
+
readonly holdout: boolean;
|
|
29
|
+
readonly metrics: Readonly<Record<string, number | boolean>>;
|
|
30
|
+
}
|
|
31
|
+
/** Versioned golden-run record persisted to the `.eval` ledger (#1703 Tier 2). */
|
|
32
|
+
export interface GoldenRunRecord {
|
|
33
|
+
readonly schemaVersion: typeof GOLDEN_RUN_SCHEMA_VERSION;
|
|
34
|
+
readonly runId: string;
|
|
35
|
+
readonly directiveVersion: string;
|
|
36
|
+
readonly model: string;
|
|
37
|
+
readonly harness: string;
|
|
38
|
+
readonly seeds: readonly number[];
|
|
39
|
+
readonly corpusVersion: string;
|
|
40
|
+
readonly recordedAt: string;
|
|
41
|
+
readonly results: readonly GoldenTaskResult[];
|
|
42
|
+
readonly summary: GoldenRunSummary;
|
|
43
|
+
}
|
|
44
|
+
/** Aggregate metrics for one golden run. */
|
|
45
|
+
export interface GoldenRunSummary {
|
|
46
|
+
readonly primaryPassRate: number;
|
|
47
|
+
readonly holdoutPassRate: number;
|
|
48
|
+
readonly passRate: number;
|
|
49
|
+
readonly primaryTotal: number;
|
|
50
|
+
readonly holdoutTotal: number;
|
|
51
|
+
}
|
|
52
|
+
export interface RunGoldenEvalOptions {
|
|
53
|
+
readonly projectRoot?: string;
|
|
54
|
+
readonly model: string;
|
|
55
|
+
readonly seeds?: readonly number[];
|
|
56
|
+
readonly directiveVersion?: string;
|
|
57
|
+
readonly harness?: string;
|
|
58
|
+
readonly persist?: boolean;
|
|
59
|
+
readonly now?: () => Date;
|
|
60
|
+
readonly mkTempDir?: () => string;
|
|
61
|
+
}
|
|
62
|
+
export interface RunGoldenEvalResult {
|
|
63
|
+
readonly code: 0 | 1 | 2;
|
|
64
|
+
readonly record: GoldenRunRecord | null;
|
|
65
|
+
readonly message: string;
|
|
66
|
+
}
|
|
67
|
+
/** Fixed golden corpus with objective graders (#1703 Tier 2). */
|
|
68
|
+
export declare const GOLDEN_CORPUS: readonly GoldenTaskDefinition[];
|
|
69
|
+
/** Absolute path to the golden-run results ledger. */
|
|
70
|
+
export declare function goldenRunsHistoryPath(projectRoot: string): string;
|
|
71
|
+
/** Append one golden run to the versioned ledger (#1703 Tier 2). */
|
|
72
|
+
export declare function persistGoldenRun(projectRoot: string, record: GoldenRunRecord): void;
|
|
73
|
+
/** Stable hash for rotating holdout selection (#1703 Goodhart mitigation). */
|
|
74
|
+
export declare function holdoutRotationIndex(directiveVersion: string, model: string, holdoutCount: number): number;
|
|
75
|
+
/** Select the active holdout task for this version × model tuple. */
|
|
76
|
+
export declare function selectRotatingHoldoutTask(directiveVersion: string, model: string): GoldenTaskDefinition | null;
|
|
77
|
+
/** Execute the golden corpus for one model × seed set and optionally persist (#1703 Tier 2). */
|
|
78
|
+
export declare function runGoldenEval(options: RunGoldenEvalOptions): RunGoldenEvalResult;
|
|
79
|
+
//# sourceMappingURL=run.d.ts.map
|