@deftai/directive-core 0.70.0 → 0.71.1
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/doctor/doctor-state.js +2 -2
- 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 +15 -5
- package/dist/intake/candidates-log.d.ts +0 -1
- package/dist/intake/candidates-log.js +5 -2
- package/dist/intake/issue-ingest.d.ts +8 -0
- package/dist/intake/issue-ingest.js +19 -1
- package/dist/intake/reconcile-issues.js +9 -2
- package/dist/layout/resolve.d.ts +2 -2
- package/dist/layout/resolve.js +2 -2
- package/dist/lifecycle/events.js +2 -0
- package/dist/orchestration/subagent-monitor.js +2 -2
- package/dist/policy/index.d.ts +2 -1
- package/dist/policy/index.js +15 -2
- package/dist/policy/value-feedback.d.ts +56 -0
- package/dist/policy/value-feedback.js +284 -0
- 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/task-surface/index.js +4 -1
- package/dist/triage/actions/candidates-log.d.ts +4 -4
- package/dist/triage/actions/candidates-log.js +4 -7
- package/dist/triage/actions/index.js +8 -32
- package/dist/triage/bootstrap/gitignore.d.ts +9 -3
- package/dist/triage/bootstrap/gitignore.js +77 -40
- package/dist/triage/bootstrap/index.d.ts +1 -1
- package/dist/triage/bootstrap/index.js +4 -3
- package/dist/triage/bulk/index.js +2 -2
- package/dist/triage/cache-path.d.ts +41 -0
- package/dist/triage/cache-path.js +115 -0
- package/dist/triage/help/registry-data.d.ts +48 -15
- package/dist/triage/help/registry-data.js +86 -15
- 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/mutations-core.js +3 -2
- 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 +2 -2
- package/dist/triage/welcome/constants.js +2 -2
- package/dist/triage/welcome/prior-state.js +3 -2
- package/dist/triage/welcome/summary.js +5 -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-validate/decomposition.js +1 -1
- package/dist/wip-cap/evaluate.js +3 -0
- package/package.json +35 -3
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { agentsRefreshPlan } from "../doctor/agents-md.js";
|
|
4
|
+
import { evaluate as evaluateEncoding } from "../encoding/evaluate.js";
|
|
5
|
+
import { readCorePackageVersion } from "../engine-version.js";
|
|
6
|
+
import { resolveEvalPath, resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
7
|
+
import { readPlanPolicy } from "../policy/plan-extensions.js";
|
|
8
|
+
import { classifyOnboarding, detectPriorState } from "../triage/welcome/prior-state.js";
|
|
9
|
+
import { validateLinks } from "../validate-content/index.js";
|
|
10
|
+
import { evaluateConformance } from "../vbrief-validate/conformance.js";
|
|
11
|
+
import { validateWipCapOnPlan } from "../vbrief-validate/plan-hooks.js";
|
|
12
|
+
import { evaluateContentManifest } from "../verify-source/content-manifest.js";
|
|
13
|
+
export const HEALTH_SCHEMA_VERSION = 1;
|
|
14
|
+
export const HEALTH_HISTORY_REL = "results/health-history.jsonl";
|
|
15
|
+
function sanitizeOneLine(value) {
|
|
16
|
+
return value.replace(/\r?\n/g, " ");
|
|
17
|
+
}
|
|
18
|
+
function isFrameworkSourceCheckout(projectRoot) {
|
|
19
|
+
const manifest = resolve(projectRoot, "conventions/content-manifest.json");
|
|
20
|
+
const cliPkg = resolve(projectRoot, "packages/cli/package.json");
|
|
21
|
+
return existsSync(manifest) && existsSync(cliPkg);
|
|
22
|
+
}
|
|
23
|
+
function loadPlan(projectRoot) {
|
|
24
|
+
const filepath = resolveProjectDefinitionPath(projectRoot);
|
|
25
|
+
if (!existsSync(filepath)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(readFileSync(filepath, "utf8"));
|
|
30
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { plan: data.plan, filepath };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function probeEncoding(projectRoot) {
|
|
40
|
+
const result = evaluateEncoding(projectRoot, { mode: "all" });
|
|
41
|
+
return {
|
|
42
|
+
id: "encoding",
|
|
43
|
+
title: "verify:encoding",
|
|
44
|
+
pass: result.exitCode === 0,
|
|
45
|
+
exitCode: result.exitCode,
|
|
46
|
+
detail: result.exitCode === 0 ? undefined : result.message.split("\n")[0],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function probeLinks(projectRoot) {
|
|
50
|
+
const result = validateLinks.evaluate({
|
|
51
|
+
cwd: projectRoot,
|
|
52
|
+
strict: false,
|
|
53
|
+
argv: [],
|
|
54
|
+
linkCheckStrict: process.env.LINK_CHECK_STRICT === "1",
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
id: "links",
|
|
58
|
+
title: "verify:links",
|
|
59
|
+
pass: result.code === 0,
|
|
60
|
+
exitCode: result.code,
|
|
61
|
+
detail: result.code === 0 ? undefined : result.message.split("\n")[0],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function probeVbriefConformance(projectRoot) {
|
|
65
|
+
const result = evaluateConformance(projectRoot, { mode: "all" });
|
|
66
|
+
return {
|
|
67
|
+
id: "vbrief-conformance",
|
|
68
|
+
title: "verify:vbrief-conformance",
|
|
69
|
+
pass: result.exitCode === 0,
|
|
70
|
+
exitCode: result.exitCode,
|
|
71
|
+
detail: result.exitCode === 0 ? undefined : result.message.split("\n")[0],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function probeAgentsMdFreshness(projectRoot) {
|
|
75
|
+
const plan = agentsRefreshPlan(projectRoot);
|
|
76
|
+
const state = String(plan.state ?? "unknown");
|
|
77
|
+
const pass = state === "current";
|
|
78
|
+
return {
|
|
79
|
+
id: "agents-md-freshness",
|
|
80
|
+
title: "AGENTS.md managed-section freshness",
|
|
81
|
+
pass,
|
|
82
|
+
exitCode: pass ? 0 : 1,
|
|
83
|
+
detail: pass ? undefined : `state=${state}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function probeContentManifest(projectRoot) {
|
|
87
|
+
const result = evaluateContentManifest(projectRoot, { root: projectRoot });
|
|
88
|
+
return {
|
|
89
|
+
id: "content-manifest",
|
|
90
|
+
title: "verify:content-manifest",
|
|
91
|
+
pass: result.code === 0,
|
|
92
|
+
exitCode: result.code,
|
|
93
|
+
detail: result.code === 0 ? undefined : result.message.split("\n")[0],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Absolute path to the versioned health history ledger. */
|
|
97
|
+
export function healthHistoryPath(projectRoot) {
|
|
98
|
+
return resolveEvalPath(projectRoot, HEALTH_HISTORY_REL);
|
|
99
|
+
}
|
|
100
|
+
/** Detect the canonical wipCap unsatisfiable-nudge contradiction (#1694). */
|
|
101
|
+
export function detectWipCapUnsatisfiableNudge(projectRoot) {
|
|
102
|
+
const loaded = loadPlan(projectRoot);
|
|
103
|
+
if (loaded === null) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const { plan, filepath } = loaded;
|
|
107
|
+
const policy = readPlanPolicy(plan);
|
|
108
|
+
const wipCapPresent = typeof policy === "object" &&
|
|
109
|
+
policy !== null &&
|
|
110
|
+
!Array.isArray(policy) &&
|
|
111
|
+
"wipCap" in policy;
|
|
112
|
+
if (wipCapPresent) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const state = detectPriorState(projectRoot);
|
|
116
|
+
const [, missing] = classifyOnboarding(state);
|
|
117
|
+
if (!missing.includes("wipCap")) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const validatorErrors = validateWipCapOnPlan(plan, filepath);
|
|
121
|
+
if (validatorErrors.length > 0) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
id: "wipCap-unsatisfiable-nudge",
|
|
126
|
+
kind: "unsatisfiable-nudge",
|
|
127
|
+
summary: "Onboarding completeness treats absent plan.policy.wipCap as incomplete, but omit-by-design accepts absence as valid (#1694 / #1186 D1).",
|
|
128
|
+
signals: [
|
|
129
|
+
"classifyOnboarding: wipCap listed in missing onboarding signals",
|
|
130
|
+
"validateWipCapOnPlan: omitted wipCap is valid",
|
|
131
|
+
"triage:welcome --onboard nudge cannot clear without violating omit-by-design contract",
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Run all registered contradictory-gate detectors. */
|
|
136
|
+
export function detectContradictoryGates(projectRoot) {
|
|
137
|
+
const wipCap = detectWipCapUnsatisfiableNudge(projectRoot);
|
|
138
|
+
return wipCap === null ? [] : [wipCap];
|
|
139
|
+
}
|
|
140
|
+
/** Compute a 0-100 score from gate pass rate minus contradiction penalty. */
|
|
141
|
+
export function computeHealthScore(gates, contradictions) {
|
|
142
|
+
const active = gates.filter((g) => !g.skipped);
|
|
143
|
+
if (active.length === 0) {
|
|
144
|
+
return contradictions.length > 0 ? 0 : 100;
|
|
145
|
+
}
|
|
146
|
+
const passed = active.filter((g) => g.pass).length;
|
|
147
|
+
const base = Math.round((passed / active.length) * 100);
|
|
148
|
+
const penalty = contradictions.length * 15;
|
|
149
|
+
return Math.max(0, base - penalty);
|
|
150
|
+
}
|
|
151
|
+
/** Append one health run to the versioned ledger (#1703 Tier 0). */
|
|
152
|
+
export function persistHealthRun(projectRoot, report) {
|
|
153
|
+
const path = healthHistoryPath(projectRoot);
|
|
154
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
155
|
+
appendFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
|
|
156
|
+
}
|
|
157
|
+
function collectStaticGates(projectRoot, frameworkSource) {
|
|
158
|
+
const gates = [
|
|
159
|
+
probeEncoding(projectRoot),
|
|
160
|
+
probeLinks(projectRoot),
|
|
161
|
+
probeVbriefConformance(projectRoot),
|
|
162
|
+
probeAgentsMdFreshness(projectRoot),
|
|
163
|
+
];
|
|
164
|
+
if (frameworkSource) {
|
|
165
|
+
gates.push(probeContentManifest(projectRoot));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
gates.push({
|
|
169
|
+
id: "content-manifest",
|
|
170
|
+
title: "verify:content-manifest",
|
|
171
|
+
pass: true,
|
|
172
|
+
exitCode: 0,
|
|
173
|
+
skipped: true,
|
|
174
|
+
skipReason: "framework-source-only gate",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return gates;
|
|
178
|
+
}
|
|
179
|
+
function formatHumanReport(report) {
|
|
180
|
+
const lines = [
|
|
181
|
+
`eval:health v${report.version} score=${report.score}/100 (${report.recordedAt})`,
|
|
182
|
+
...report.gates.map((g) => {
|
|
183
|
+
const status = g.skipped ? "SKIP" : g.pass ? "PASS" : "FAIL";
|
|
184
|
+
const suffix = g.skipped
|
|
185
|
+
? ` (${sanitizeOneLine(g.skipReason ?? "")})`
|
|
186
|
+
: g.detail
|
|
187
|
+
? ` -- ${sanitizeOneLine(g.detail)}`
|
|
188
|
+
: "";
|
|
189
|
+
return ` [${status}] ${g.title}${suffix}`;
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
if (report.contradictions.length > 0) {
|
|
193
|
+
lines.push(" Contradictory gates:");
|
|
194
|
+
for (const c of report.contradictions) {
|
|
195
|
+
lines.push(` - ${c.id}: ${sanitizeOneLine(c.summary)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
/** Aggregate Tier-0 static gates into a versioned framework health score (#1703). */
|
|
201
|
+
export function evaluateHealth(options = {}) {
|
|
202
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
203
|
+
const frameworkSource = options.frameworkSource ?? isFrameworkSourceCheckout(projectRoot);
|
|
204
|
+
const now = options.now ?? (() => new Date());
|
|
205
|
+
const persist = options.persist ?? true;
|
|
206
|
+
const gates = collectStaticGates(projectRoot, frameworkSource);
|
|
207
|
+
const contradictions = detectContradictoryGates(projectRoot);
|
|
208
|
+
const score = computeHealthScore(gates, contradictions);
|
|
209
|
+
const report = {
|
|
210
|
+
schemaVersion: HEALTH_SCHEMA_VERSION,
|
|
211
|
+
version: readCorePackageVersion(),
|
|
212
|
+
recordedAt: now()
|
|
213
|
+
.toISOString()
|
|
214
|
+
.replace(/\.\d{3}Z$/, "Z"),
|
|
215
|
+
score,
|
|
216
|
+
gates,
|
|
217
|
+
contradictions,
|
|
218
|
+
};
|
|
219
|
+
if (persist) {
|
|
220
|
+
try {
|
|
221
|
+
persistHealthRun(projectRoot, report);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const persistError = `eval:health: failed to persist health history: ${String(err)}`;
|
|
225
|
+
const healthy = score === 100 && contradictions.length === 0;
|
|
226
|
+
return {
|
|
227
|
+
code: healthy ? 0 : 1,
|
|
228
|
+
report,
|
|
229
|
+
message: `${formatHumanReport(report)}\n${persistError}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const healthy = score === 100 && contradictions.length === 0;
|
|
234
|
+
return {
|
|
235
|
+
code: healthy ? 0 : 1,
|
|
236
|
+
report,
|
|
237
|
+
message: formatHumanReport(report),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type HealthReport } from "./health.js";
|
|
2
|
+
/** Repeat-suppression window for the budgeted eval session nudge (#1703 / #1279 parity). */
|
|
3
|
+
export declare const EVAL_READBACK_SUPPRESSION_HOURS = 4;
|
|
4
|
+
export declare const EVAL_READBACK_HISTORY_REL: string;
|
|
5
|
+
export declare const EVAL_READBACK_HISTORY_SCHEMA = "deft.eval.readback.v1";
|
|
6
|
+
export interface SessionEvalReadbackResult {
|
|
7
|
+
readonly line: string | null;
|
|
8
|
+
readonly suppressed: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Stable nudge key for debounce — score + contradiction ids. */
|
|
11
|
+
export declare function evalReadbackNudgeKey(report: HealthReport): string;
|
|
12
|
+
/** True when the same eval nudge was read back within the suppression window. */
|
|
13
|
+
export declare function shouldSuppressEvalReadback(nudgeKey: string, historyFile: string, options?: {
|
|
14
|
+
now?: Date;
|
|
15
|
+
}): boolean;
|
|
16
|
+
/** Whether the current health report warrants a session-start advisory nudge (#2336). */
|
|
17
|
+
export declare function shouldNudgeEvalHealth(report: HealthReport, previous: HealthReport | null): boolean;
|
|
18
|
+
export declare function formatEvalHealthSessionLine(report: HealthReport, previous: HealthReport | null): string;
|
|
19
|
+
export interface RenderSessionEvalReadbackOptions {
|
|
20
|
+
readonly now?: Date;
|
|
21
|
+
readonly maxChars?: number;
|
|
22
|
+
readonly writeHistory?: boolean;
|
|
23
|
+
readonly evaluate?: (projectRoot: string) => {
|
|
24
|
+
report: HealthReport | null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Budgeted session eval one-liner — silent when healthy or debounced (#1703 / #2336). */
|
|
28
|
+
export declare function renderSessionEvalReadback(projectRoot: string, options?: RenderSessionEvalReadbackOptions): SessionEvalReadbackResult;
|
|
29
|
+
/** Emit the eval session readback line when present. Returns the line or null. */
|
|
30
|
+
export declare function emitSessionEvalReadback(projectRoot: string, options?: {
|
|
31
|
+
output?: (line: string) => void;
|
|
32
|
+
now?: Date;
|
|
33
|
+
writeHistory?: boolean;
|
|
34
|
+
evaluate?: (projectRoot: string) => {
|
|
35
|
+
report: HealthReport | null;
|
|
36
|
+
};
|
|
37
|
+
}): string | null;
|
|
38
|
+
//# sourceMappingURL=readback.d.ts.map
|
|
@@ -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
|