dsh-continual-evolve 0.1.0 → 0.2.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/README.md +149 -19
- package/README.zh.md +64 -21
- package/lib/apply.js +2 -0
- package/lib/approval.d.ts +19 -0
- package/lib/auto.d.ts +61 -1
- package/lib/auto.js +108 -2
- package/lib/benchmark.d.ts +14 -0
- package/lib/command.js +234 -5
- package/lib/evaluate.d.ts +36 -7
- package/lib/evaluate.js +157 -43
- package/lib/fate.d.ts +126 -0
- package/lib/fate.js +338 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +18 -2
- package/lib/mount.js +5 -0
- package/lib/planner.d.ts +8 -1
- package/lib/planner.js +27 -0
- package/lib/render.js +2 -1
- package/lib/review.d.ts +1 -1
- package/lib/review.js +17 -0
- package/lib/score.d.ts +22 -4
- package/lib/score.js +48 -7
- package/lib/skill.d.ts +10 -2
- package/lib/skill.js +34 -2
- package/lib/skillquality.d.ts +81 -0
- package/lib/skillquality.js +311 -0
- package/lib/tool.js +6 -3
- package/lib/types.d.ts +31 -0
- package/lib/types.js +19 -0
- package/lib/validate.js +25 -1
- package/lib/wrapup.d.ts +210 -0
- package/lib/wrapup.js +439 -0
- package/package.json +24 -14
package/lib/fate.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
|
|
3
|
+
/** Turns a declined local-fate proposal stays silent before being offered again. */
|
|
4
|
+
export const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
5
|
+
/**
|
|
6
|
+
* Partition an assessed wrap-up classification into concrete fate actions,
|
|
7
|
+
* re-running the deterministic guards against the LIVE global store (state
|
|
8
|
+
* may have changed while the LLM call was in flight). Pure and unit-tested;
|
|
9
|
+
* mirrors the partition step of the wrap-up command.
|
|
10
|
+
*/
|
|
11
|
+
export function planLocalFates(items, candidates, globalState) {
|
|
12
|
+
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
13
|
+
const { promotable, skipped } = filterPromotable(items, globalState, candidates);
|
|
14
|
+
const promoteItems = promotable.filter((item) => item.verdict === "promote");
|
|
15
|
+
const archiveItems = items.filter((item) => item.verdict === "archive");
|
|
16
|
+
const splits = [];
|
|
17
|
+
const splitSkipped = [];
|
|
18
|
+
for (const item of archiveItems) {
|
|
19
|
+
if (!item.promote)
|
|
20
|
+
continue;
|
|
21
|
+
const candidate = byKey.get(item.key);
|
|
22
|
+
if (!candidate) {
|
|
23
|
+
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const blocked = splitPromoteBlocked(item, globalState, candidate.kind);
|
|
27
|
+
if (blocked) {
|
|
28
|
+
splitSkipped.push({ key: item.key, reason: blocked });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
splits.push({ item, candidate });
|
|
32
|
+
}
|
|
33
|
+
const plainArchives = archiveItems.filter((item) => !item.promote);
|
|
34
|
+
const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
|
|
35
|
+
return { candidates, promotable: promoteItems, splits, silentArchives, reviewArchives, skipped, splitSkipped };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The cooldown key of a candidate set: the sorted `kind:id` list. The set is
|
|
39
|
+
* the unit of consultation — a declined proposal is not offered again within
|
|
40
|
+
* the cooldown window, and a changed set (new entries appeared) starts a
|
|
41
|
+
* fresh consultation.
|
|
42
|
+
*/
|
|
43
|
+
export function fateSetKey(candidates) {
|
|
44
|
+
return candidates
|
|
45
|
+
.map((candidate) => candidateKey(candidate.kind, candidate.id))
|
|
46
|
+
.sort()
|
|
47
|
+
.join("|");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether the local-fate dimension is due for this gate run. Turn-interval
|
|
51
|
+
* gates respect the fate cadence (an independent counter — goal-driven
|
|
52
|
+
* sessions run the review EVERY round, the fate assessment must not);
|
|
53
|
+
* compaction is unconditional: experiences about to be summarized away get
|
|
54
|
+
* their fate check regardless.
|
|
55
|
+
*/
|
|
56
|
+
export function fateCadenceDue(state, reason, intervalTurns) {
|
|
57
|
+
if (reason === "compact")
|
|
58
|
+
return true;
|
|
59
|
+
return state.turns - state.lastFateAt >= intervalTurns;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Ask the user whether to execute the gate's local-fate proposal. ONE dialog
|
|
63
|
+
* covers every governed action (promotes, split promotions, review-required
|
|
64
|
+
* archives) — the gate never spams questions. Conservative on every edge:
|
|
65
|
+
* no question service → not approved; the question call fails → not approved;
|
|
66
|
+
* the same candidate set was declined within the cooldown → not asked again.
|
|
67
|
+
* A decline records the cooldown (the consultSkillEdits pattern).
|
|
68
|
+
*/
|
|
69
|
+
export async function consultLocalFates(ctx, agent, plan, gate) {
|
|
70
|
+
const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
|
|
71
|
+
if (!needsDialog) {
|
|
72
|
+
return { approved: true, asked: false, reason: "nothing-to-ask" };
|
|
73
|
+
}
|
|
74
|
+
const setKey = fateSetKey(plan.candidates);
|
|
75
|
+
const lastReject = gate.fateRejects.get(setKey);
|
|
76
|
+
if (lastReject !== undefined && gate.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
|
|
77
|
+
return { approved: false, asked: false, reason: "cooldown" };
|
|
78
|
+
}
|
|
79
|
+
const userQuestions = ctx.userQuestions;
|
|
80
|
+
if (!userQuestions) {
|
|
81
|
+
return { approved: false, asked: false, reason: "unavailable" };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const answer = await userQuestions.ask({
|
|
85
|
+
questions: [
|
|
86
|
+
{
|
|
87
|
+
id: "evolve-fate-consult",
|
|
88
|
+
question: consultQuestion(plan),
|
|
89
|
+
options: [{ label: "执行" }, { label: "不执行" }],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
agent,
|
|
93
|
+
});
|
|
94
|
+
const approved = answer.answers?.find((entry) => entry.id === "evolve-fate-consult")?.selected?.includes("执行") ?? false;
|
|
95
|
+
if (!approved) {
|
|
96
|
+
gate.fateRejects.set(setKey, gate.turns);
|
|
97
|
+
}
|
|
98
|
+
return { approved, asked: true, reason: approved ? "consented" : "declined" };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { approved: false, asked: true, reason: "error" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** The user-visible fate proposal: every governed action, with real titles. */
|
|
105
|
+
function consultQuestion(plan) {
|
|
106
|
+
const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
107
|
+
const lines = [];
|
|
108
|
+
if (plan.promotable.length > 0 || plan.splits.length > 0) {
|
|
109
|
+
lines.push("【提升到跨会话全局 store】");
|
|
110
|
+
for (const item of plan.promotable) {
|
|
111
|
+
lines.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
|
|
112
|
+
}
|
|
113
|
+
for (const { item } of plan.splits) {
|
|
114
|
+
lines.push(`- ${item.key} → 拆出提升「${item.promote?.title}」(原条目随之归档)`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (plan.reviewArchives.length > 0) {
|
|
118
|
+
lines.push("【归档(未被全局覆盖且源自真实对话,需确认)】");
|
|
119
|
+
for (const item of plan.reviewArchives) {
|
|
120
|
+
lines.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [
|
|
124
|
+
"自进化门禁检测到本会话的 local 条目需要归宿处理(提升条目将写入跨会话全局 store,归档条目隐藏但可恢复):",
|
|
125
|
+
...lines,
|
|
126
|
+
"是否执行?",
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Deterministically apply the fate plan. `"full"` applies everything
|
|
131
|
+
* (promotes, splits, silent + review archives); `"silent-only"` applies only
|
|
132
|
+
* the deterministic silent archives (the compaction path — nothing governed).
|
|
133
|
+
* Promotes go through the SAME proposals as the wrap-up command
|
|
134
|
+
* (wholePromoteProposals / splitPromoteProposals), so both paths write
|
|
135
|
+
* identical global entries and local retirement stamps.
|
|
136
|
+
*/
|
|
137
|
+
export function applyLocalFates(engine, sessionId, plan, localState, mode) {
|
|
138
|
+
const applied = [];
|
|
139
|
+
const results = [];
|
|
140
|
+
const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
141
|
+
if (mode === "full") {
|
|
142
|
+
for (const item of plan.promotable) {
|
|
143
|
+
const candidate = byKey.get(item.key);
|
|
144
|
+
if (!candidate)
|
|
145
|
+
continue;
|
|
146
|
+
const proposals = wholePromoteProposals(item, candidate, sessionId);
|
|
147
|
+
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
148
|
+
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
149
|
+
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
150
|
+
scope: "local",
|
|
151
|
+
baselineState: localState,
|
|
152
|
+
});
|
|
153
|
+
results.push(globalResult, localResult);
|
|
154
|
+
applied.push(`提升 ${item.key} → 全局 store(global:${createdId},本地副本已归档)`);
|
|
155
|
+
}
|
|
156
|
+
for (const { item, candidate } of plan.splits) {
|
|
157
|
+
if (!item.promote)
|
|
158
|
+
continue;
|
|
159
|
+
const proposals = splitPromoteProposals(item, candidate, sessionId);
|
|
160
|
+
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
161
|
+
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
162
|
+
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
163
|
+
scope: "local",
|
|
164
|
+
baselineState: localState,
|
|
165
|
+
});
|
|
166
|
+
results.push(globalResult, localResult);
|
|
167
|
+
applied.push(`拆解 ${item.key} → 清洗「${item.promote.title}」入全局(原条目已归档)`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const archives = mode === "full" ? [...plan.silentArchives, ...plan.reviewArchives] : plan.silentArchives;
|
|
171
|
+
for (const item of archives) {
|
|
172
|
+
const candidate = byKey.get(item.key);
|
|
173
|
+
if (!candidate)
|
|
174
|
+
continue;
|
|
175
|
+
const proposal = {
|
|
176
|
+
summary: `gate fate: archive local ${item.key} — ${item.reason}`,
|
|
177
|
+
rationale: item.reason,
|
|
178
|
+
expectedOutcome: "The entry stops being injected but stays restorable.",
|
|
179
|
+
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
180
|
+
};
|
|
181
|
+
const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
|
|
182
|
+
results.push(result);
|
|
183
|
+
applied.push(`归档 ${item.key}「${candidate.title}」`);
|
|
184
|
+
}
|
|
185
|
+
return { applied, results };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The gate's local-fate phase. Runs after the review phase on every gate
|
|
189
|
+
* trigger (turn_interval / compact), subject to cadence and cooldown. All
|
|
190
|
+
* failures are contained and recorded — a broken fate dimension never
|
|
191
|
+
* disturbs the agent loop.
|
|
192
|
+
*/
|
|
193
|
+
export async function runLocalFatePhase(ctx, engine, agent, config, state, reason, record) {
|
|
194
|
+
if (!config.localFate)
|
|
195
|
+
return;
|
|
196
|
+
const logger = ctx.logger("continual-evolve");
|
|
197
|
+
const sessionId = agent.id;
|
|
198
|
+
const localState = engine.load("local", sessionId);
|
|
199
|
+
const globalState = engine.load("global", undefined);
|
|
200
|
+
const candidates = listLocalCandidates(localState, globalState);
|
|
201
|
+
if (candidates.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
if (!fateCadenceDue(state, reason, config.fateIntervalTurns))
|
|
204
|
+
return;
|
|
205
|
+
const setKey = fateSetKey(candidates);
|
|
206
|
+
const lastReject = state.fateRejects.get(setKey);
|
|
207
|
+
if (lastReject !== undefined && state.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
|
|
208
|
+
logger.info(`auto-review local-fate skipped [${sessionId}]: candidate set declined ${state.turns - lastReject} turns ago (cooldown)`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const turnsSinceFate = state.turns - state.lastFateAt;
|
|
212
|
+
state.lastFateAt = state.turns;
|
|
213
|
+
let assessment;
|
|
214
|
+
try {
|
|
215
|
+
assessment = await assessLocalEntries(ctx, agent, candidates);
|
|
216
|
+
}
|
|
217
|
+
catch (cause) {
|
|
218
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
219
|
+
logger.warn(`auto-review local-fate failed for ${sessionId}: ${message}`);
|
|
220
|
+
record({
|
|
221
|
+
sessionId,
|
|
222
|
+
reason,
|
|
223
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
224
|
+
outcome: "failed",
|
|
225
|
+
rationale: `fate assessment error: ${message}`,
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const plan = planLocalFates(assessment.items, candidates, globalState);
|
|
230
|
+
const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
|
|
231
|
+
let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
|
|
232
|
+
if (reason !== "compact" && needsDialog) {
|
|
233
|
+
consent = await consultLocalFates(ctx, agent, plan, state);
|
|
234
|
+
}
|
|
235
|
+
if (consent.approved) {
|
|
236
|
+
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "full");
|
|
237
|
+
logger.info(`auto-review local-fate approved (${reason}) [${sessionId}]: ${plan.promotable.length} promoted, ${plan.splits.length} split, ${plan.reviewArchives.length} review-archived, ${plan.silentArchives.length} silent-archived — ${assessment.rationale}`);
|
|
238
|
+
record({
|
|
239
|
+
sessionId,
|
|
240
|
+
reason,
|
|
241
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
242
|
+
outcome: "approved",
|
|
243
|
+
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
244
|
+
refinementId: results.map((result) => result.id).join(","),
|
|
245
|
+
});
|
|
246
|
+
if (config.notifyOnAutoReview && reason === "turn_interval" && applied.length > 0) {
|
|
247
|
+
notifyFateApplied(ctx, agent, applied);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (consent.reason === "declined" || consent.reason === "unavailable" || consent.reason === "error") {
|
|
252
|
+
const withheld = consent.reason;
|
|
253
|
+
const outcome = withheld === "declined" ? "declined" : "deferred";
|
|
254
|
+
logger.info(`auto-review local-fate ${outcome} (${reason}) [${sessionId}]: ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld — ${assessment.rationale}`);
|
|
255
|
+
record({
|
|
256
|
+
sessionId,
|
|
257
|
+
reason,
|
|
258
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
259
|
+
outcome,
|
|
260
|
+
rationale: `fate ${withheld}: ${assessment.rationale} (${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld)`,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (reason === "compact" && needsDialog) {
|
|
265
|
+
// Compaction: no dialog. Only deterministic silent archives apply;
|
|
266
|
+
// governed actions are deferred with an audit record.
|
|
267
|
+
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
|
|
268
|
+
if (applied.length > 0) {
|
|
269
|
+
logger.info(`auto-review local-fate (compact) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
|
|
270
|
+
record({
|
|
271
|
+
sessionId,
|
|
272
|
+
reason,
|
|
273
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
274
|
+
outcome: "approved",
|
|
275
|
+
rationale: `fate (compact): ${assessment.rationale} (${applied.join("; ")})`,
|
|
276
|
+
refinementId: results.map((result) => result.id).join(","),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
record({
|
|
280
|
+
sessionId,
|
|
281
|
+
reason,
|
|
282
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
283
|
+
outcome: "deferred",
|
|
284
|
+
rationale: `fate (compact): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Silent archives on a plain turn-interval gate with nothing to ask.
|
|
289
|
+
if (plan.silentArchives.length > 0) {
|
|
290
|
+
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
|
|
291
|
+
logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} covered/operational entries — ${assessment.rationale}`);
|
|
292
|
+
record({
|
|
293
|
+
sessionId,
|
|
294
|
+
reason,
|
|
295
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
296
|
+
outcome: "approved",
|
|
297
|
+
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
298
|
+
refinementId: results.map((result) => result.id).join(","),
|
|
299
|
+
});
|
|
300
|
+
if (config.notifyOnAutoReview && reason === "turn_interval") {
|
|
301
|
+
notifyFateApplied(ctx, agent, applied);
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Assessed, nothing to do.
|
|
306
|
+
logger.info(`auto-review local-fate assessed (${reason}) [${sessionId}]: ${candidates.length} candidates, no action — ${assessment.rationale}`);
|
|
307
|
+
record({
|
|
308
|
+
sessionId,
|
|
309
|
+
reason,
|
|
310
|
+
turnsSinceLastReview: turnsSinceFate,
|
|
311
|
+
outcome: "assessed",
|
|
312
|
+
rationale: `fate: ${assessment.rationale} (${candidates.length} candidates, no action)`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** The user-visible notice after the gate applied local-fate actions. */
|
|
316
|
+
export function buildFateNotice(applied) {
|
|
317
|
+
return [
|
|
318
|
+
"🔎 自动进化门禁:本会话 local 条目归宿处理完成:",
|
|
319
|
+
applied.map((line) => `- ${line}`).join("\n"),
|
|
320
|
+
"查看全部条目:/evolve list;撤销:/evolve rollback <refinement id>",
|
|
321
|
+
"请用一句话简短确认即可,不要调用任何工具。",
|
|
322
|
+
].join("\n");
|
|
323
|
+
}
|
|
324
|
+
/** Queue the follow-up notice turn (turn_interval only, like the review notice). */
|
|
325
|
+
function notifyFateApplied(ctx, agent, applied) {
|
|
326
|
+
try {
|
|
327
|
+
agent.followup(createUserMessage({
|
|
328
|
+
content: [{ type: "text", text: buildFateNotice(applied) }],
|
|
329
|
+
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
catch (cause) {
|
|
333
|
+
ctx
|
|
334
|
+
.logger("continual-evolve")
|
|
335
|
+
.warn(`local-fate notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=fate.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -34,6 +34,17 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
34
34
|
logMaxBytes: z<number, number>;
|
|
35
35
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
36
36
|
autoRollbackOnReject: z<boolean, boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
39
|
+
* entries on its own cadence and proposes promote/archive — consulted
|
|
40
|
+
* first, never written silently. Only meaningful with autoReview on.
|
|
41
|
+
*/
|
|
42
|
+
localFate: z<boolean, boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Minimum turns between local-fate assessments on the turn-interval path
|
|
45
|
+
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
46
|
+
*/
|
|
47
|
+
fateIntervalTurns: z<number, number>;
|
|
37
48
|
}>, Schemastery.ObjectT<{
|
|
38
49
|
/** Root for evolution stores; defaults to the resolved DSH home. */
|
|
39
50
|
baseDir: z<string, string>;
|
|
@@ -63,6 +74,17 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
63
74
|
logMaxBytes: z<number, number>;
|
|
64
75
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
65
76
|
autoRollbackOnReject: z<boolean, boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
79
|
+
* entries on its own cadence and proposes promote/archive — consulted
|
|
80
|
+
* first, never written silently. Only meaningful with autoReview on.
|
|
81
|
+
*/
|
|
82
|
+
localFate: z<boolean, boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* Minimum turns between local-fate assessments on the turn-interval path
|
|
85
|
+
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
86
|
+
*/
|
|
87
|
+
fateIntervalTurns: z<number, number>;
|
|
66
88
|
}>>;
|
|
67
89
|
/** Structurally typed resolved config (loader passes the validated object). */
|
|
68
90
|
export interface EvolveConfig {
|
|
@@ -84,6 +106,10 @@ export interface EvolveConfig {
|
|
|
84
106
|
logMaxBytes?: number;
|
|
85
107
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
86
108
|
autoRollbackOnReject?: boolean;
|
|
109
|
+
/** Gate local-fate dimension: propose promote/archive of local entries automatically. */
|
|
110
|
+
localFate?: boolean;
|
|
111
|
+
/** Minimum turns between local-fate assessments (compaction is unconditional). */
|
|
112
|
+
fateIntervalTurns?: number;
|
|
87
113
|
}
|
|
88
114
|
export interface EvolutionService {
|
|
89
115
|
readonly engine: EvolutionEngine;
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,17 @@ export const Config = z.object({
|
|
|
52
52
|
logMaxBytes: z.natural().default(5 * 1024 * 1024),
|
|
53
53
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
54
54
|
autoRollbackOnReject: z.boolean().default(true),
|
|
55
|
+
/**
|
|
56
|
+
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
57
|
+
* entries on its own cadence and proposes promote/archive — consulted
|
|
58
|
+
* first, never written silently. Only meaningful with autoReview on.
|
|
59
|
+
*/
|
|
60
|
+
localFate: z.boolean().default(true),
|
|
61
|
+
/**
|
|
62
|
+
* Minimum turns between local-fate assessments on the turn-interval path
|
|
63
|
+
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
64
|
+
*/
|
|
65
|
+
fateIntervalTurns: z.natural(),
|
|
55
66
|
});
|
|
56
67
|
export function apply(ctx, config) {
|
|
57
68
|
const baseDir = resolveDshHome(config.baseDir);
|
|
@@ -59,7 +70,10 @@ export function apply(ctx, config) {
|
|
|
59
70
|
const engine = createEvolutionEngine(baseDir, {
|
|
60
71
|
onApplied: (result) => {
|
|
61
72
|
try {
|
|
62
|
-
syncSkillsFromResult(skillsRoot, result);
|
|
73
|
+
const warnings = syncSkillsFromResult(skillsRoot, result);
|
|
74
|
+
for (const warning of warnings) {
|
|
75
|
+
ctx.logger("continual-evolve").warn(warning);
|
|
76
|
+
}
|
|
63
77
|
}
|
|
64
78
|
catch (cause) {
|
|
65
79
|
ctx
|
|
@@ -108,8 +122,10 @@ export function apply(ctx, config) {
|
|
|
108
122
|
maxInputChars: config.maxReviewInputChars ?? 40000,
|
|
109
123
|
budgetTokens: config.reviewBudgetTokens ?? 4096,
|
|
110
124
|
notifyOnAutoReview: config.notifyOnAutoReview ?? true,
|
|
125
|
+
localFate: config.localFate ?? true,
|
|
126
|
+
fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
|
|
111
127
|
});
|
|
112
|
-
ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns)`);
|
|
128
|
+
ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
|
|
113
129
|
}
|
|
114
130
|
ctx.logger("continual-evolve").info(`continual-evolve mounted (baseDir=${baseDir})`);
|
|
115
131
|
}
|
package/lib/mount.js
CHANGED
|
@@ -124,6 +124,11 @@ export function renderParameters(entry) {
|
|
|
124
124
|
* the ledger records the entry for the next boot.
|
|
125
125
|
*/
|
|
126
126
|
export async function mountSkill(ctx, baseDir, entry) {
|
|
127
|
+
// Guidance skills are SKILL.md documents with no python reference — there
|
|
128
|
+
// is no function to mount as a tool; only executable skills can hot-mount.
|
|
129
|
+
if (entry.skill_kind === "guidance" || Object.keys(entry.reference ?? {}).length === 0) {
|
|
130
|
+
throw new Error(`skill ${entry.id} has no python reference (guidance skills cannot be mounted — load them with the skill tool instead)`);
|
|
131
|
+
}
|
|
127
132
|
const dir = renderMountPackage(baseDir, entry);
|
|
128
133
|
const entryId = `evolve-mount-${skillNameOf(entry.id)}`;
|
|
129
134
|
const loader = ctx.get("loader");
|
package/lib/planner.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { Context } from "@deepseek-ai/cordis";
|
|
10
10
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
11
11
|
import type { HarnessState, RefinementProposal, RefinementResult } from "./types.js";
|
|
12
|
-
export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;\n skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Local edits are session-scoped; global edits persist across sessions.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"metadata\": {},\n \"reason\": \"why this edit is useful\"\n }\n ]\n}";
|
|
12
|
+
export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;\n skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Skill entries are authored to the DSH skill quality standard\n (skill-creator, distilled from the official deepseek-harness 11 skills;\n the full facts are provided in the <skill_quality_standard> block below):\n only for a REAL trigger scenario grounded in the trajectory\n (who, in what real task, what signal) \u2014 never invent one to pad the store;\n never duplicate the official 11 skills or existing entries; content is a\n SKILL.md document (frontmatter routing with \"use when / do not use when\"\n description, boundary declaration, prerequisites and exclusions, layered\n information, verifiable completion criteria). Self-check every proposed\n skill against the 7 structural features and state the result in its\n reason field.\n- Repeated multi-step workflows (session start/end routines, recurring\n wrap-up or handoff procedures) may be proposed as guidance skills:\n kind=skill, skill_kind=\"guidance\", content = a SKILL.md document (no\n python reference \u2014 executable skills keep requiring reference +\n arguments). Only propose with repeated evidence in the trajectory, never\n for one-off flows. Guidance skills materialize as discoverable SKILL.md\n files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to\n the user for a decision before they land.\n- Local edits are session-scoped; global edits persist across sessions.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"skill_kind\": \"executable|guidance (optional; skill kind only \u2014 guidance = SKILL.md document without reference)\",\n \"metadata\": {},\n \"reason\": \"why this edit is useful\"\n }\n ]\n}";
|
|
13
13
|
export interface PlanOptions {
|
|
14
14
|
agent: Agent;
|
|
15
15
|
state: HarnessState;
|
|
@@ -22,6 +22,13 @@ export interface PlanOptions {
|
|
|
22
22
|
* every planning call is grounded in what the user actually said.
|
|
23
23
|
*/
|
|
24
24
|
trajectory?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Skills root to read the skill-creator template facts from
|
|
27
|
+
* (`<root>/skill-creator/references/template.md`). When omitted or the
|
|
28
|
+
* skills are not installed, the builtin distilled quality guide is
|
|
29
|
+
* injected instead — the skill standard is always present.
|
|
30
|
+
*/
|
|
31
|
+
skillsRoot?: string;
|
|
25
32
|
global?: boolean;
|
|
26
33
|
signal?: AbortSignal;
|
|
27
34
|
maxOutputTokens?: number;
|
package/lib/planner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-
|
|
|
2
2
|
import { parseProposal } from "./plan.js";
|
|
3
3
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
4
4
|
import { recentUserText } from "./inject.js";
|
|
5
|
+
import { skillQualityGuide } from "./skillquality.js";
|
|
5
6
|
export const PLANNER_SYSTEM_PROMPT = `You are the /evolve continual harness subsystem.
|
|
6
7
|
|
|
7
8
|
Your job is to improve the editable continual harness state. Instead of
|
|
@@ -14,6 +15,25 @@ Rules:
|
|
|
14
15
|
- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;
|
|
15
16
|
skill = repeatable procedures (must carry a python reference {type:"python", import, callable}
|
|
16
17
|
and an arguments object); subagent = reusable delegation roles.
|
|
18
|
+
- Skill entries are authored to the DSH skill quality standard
|
|
19
|
+
(skill-creator, distilled from the official deepseek-harness 11 skills;
|
|
20
|
+
the full facts are provided in the <skill_quality_standard> block below):
|
|
21
|
+
only for a REAL trigger scenario grounded in the trajectory
|
|
22
|
+
(who, in what real task, what signal) — never invent one to pad the store;
|
|
23
|
+
never duplicate the official 11 skills or existing entries; content is a
|
|
24
|
+
SKILL.md document (frontmatter routing with "use when / do not use when"
|
|
25
|
+
description, boundary declaration, prerequisites and exclusions, layered
|
|
26
|
+
information, verifiable completion criteria). Self-check every proposed
|
|
27
|
+
skill against the 7 structural features and state the result in its
|
|
28
|
+
reason field.
|
|
29
|
+
- Repeated multi-step workflows (session start/end routines, recurring
|
|
30
|
+
wrap-up or handoff procedures) may be proposed as guidance skills:
|
|
31
|
+
kind=skill, skill_kind="guidance", content = a SKILL.md document (no
|
|
32
|
+
python reference — executable skills keep requiring reference +
|
|
33
|
+
arguments). Only propose with repeated evidence in the trajectory, never
|
|
34
|
+
for one-off flows. Guidance skills materialize as discoverable SKILL.md
|
|
35
|
+
files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to
|
|
36
|
+
the user for a decision before they land.
|
|
17
37
|
- Local edits are session-scoped; global edits persist across sessions.
|
|
18
38
|
- Ground every edit in evidence: the session trajectory (recent direct user
|
|
19
39
|
messages) is provided when available; prefer edits backed by it over
|
|
@@ -37,6 +57,7 @@ Rules:
|
|
|
37
57
|
"path": "optional grouping path",
|
|
38
58
|
"reference": {"type":"python","import":"pkg.mod","callable":"fn"} ,
|
|
39
59
|
"arguments": {"name": {"type":"string","required":true,"description":"..."}},
|
|
60
|
+
"skill_kind": "executable|guidance (optional; skill kind only — guidance = SKILL.md document without reference)",
|
|
40
61
|
"metadata": {},
|
|
41
62
|
"reason": "why this edit is useful"
|
|
42
63
|
}
|
|
@@ -54,11 +75,17 @@ export async function planWithLlm(ctx, options) {
|
|
|
54
75
|
// most recent direct user messages ("" when none qualify — the block is
|
|
55
76
|
// then omitted entirely, keeping an empty trajectory zero-cost).
|
|
56
77
|
const trajectory = options.trajectory ?? recentUserText(agent);
|
|
78
|
+
// The skill quality standard is always present: the skill-creator
|
|
79
|
+
// template facts when installed, the builtin distilled guide otherwise
|
|
80
|
+
// (~1KB — planning is low-frequency, and the standard keeps skill
|
|
81
|
+
// proposals from drifting off the quality bar).
|
|
82
|
+
const qualityGuide = skillQualityGuide(options.skillsRoot);
|
|
57
83
|
const userPrompt = [
|
|
58
84
|
`<current_harness_state>\n${formatHarnessStateForPrompt(state)}\n</current_harness_state>`,
|
|
59
85
|
`<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
|
|
60
86
|
`<scope_policy>\n${scopeInstruction}\n</scope_policy>`,
|
|
61
87
|
trajectory ? `<session_trajectory>\n${trajectory}\n</session_trajectory>` : "",
|
|
88
|
+
`<skill_quality_standard>\n${qualityGuide.text}\n</skill_quality_standard>`,
|
|
62
89
|
options.instructions ? `<user_instructions>\n${options.instructions}\n</user_instructions>` : "",
|
|
63
90
|
"Return only JSON edits. If no useful edit is justified, return an empty edits array with a rationale.",
|
|
64
91
|
]
|
package/lib/render.js
CHANGED
|
@@ -18,7 +18,8 @@ export function entryLine(entry, maxContentLength) {
|
|
|
18
18
|
: "";
|
|
19
19
|
const citationText = citationSuffix(entry);
|
|
20
20
|
const archivedText = isArchived(entry) ? " [archived]" : "";
|
|
21
|
-
|
|
21
|
+
const formText = entry.kind === "skill" && entry.skill_kind === "guidance" ? " [guidance]" : "";
|
|
22
|
+
return `- [${entry.scope}:${entry.id}] ${entry.title} (${entry.path}, v${entry.version})${archivedText}${formText}${referenceText}${argumentsText}${citationText}: ${compactText(entry.content, maxContentLength)}`;
|
|
22
23
|
}
|
|
23
24
|
/** Trajectory citation suffix (` src=sessionId:1,2`), empty when uncited. */
|
|
24
25
|
function citationSuffix(entry) {
|
package/lib/review.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface ReviewOptions {
|
|
|
28
28
|
signal?: AbortSignal;
|
|
29
29
|
budgetTokens?: number;
|
|
30
30
|
}
|
|
31
|
-
export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should run /evolve. Auto /evolve writes local\nharness state by default, so approve when the trajectory contains evidence\nuseful to this session's future turns: a repeated failure, a reusable tactic,\na repeated delegation role, a durable fact or preference, a user correction\nthat should persist, or a narrow behavioral policy.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
|
|
31
|
+
export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should run /evolve. Auto /evolve writes local\nharness state by default, so approve when the trajectory contains evidence\nuseful to this session's future turns: a repeated failure, a reusable tactic,\na repeated delegation role, a durable fact or preference, a user correction\nthat should persist, or a narrow behavioral policy.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
|
|
32
32
|
/** Parse the gate's JSON reply. */
|
|
33
33
|
export declare function parseAutoRefineReview(text: string): AutoRefineReview;
|
|
34
34
|
/** Serialize surface events to bounded role-prefixed text. */
|
package/lib/review.js
CHANGED
|
@@ -22,6 +22,23 @@ refine target: approve with instructions naming the entry ids, and tell the
|
|
|
22
22
|
planner to archive them (archive hides from injection, data stays restorable)
|
|
23
23
|
rather than delete.
|
|
24
24
|
|
|
25
|
+
Skill-related trajectories (the evidence concerns creating or improving a
|
|
26
|
+
skill entry) are judged against the DSH skill quality standard (skill-audit
|
|
27
|
+
dimensions: frontmatter routing, the 7 structural features, paragraph
|
|
28
|
+
skeleton, no duplication of the official 11 skills or covered skills).
|
|
29
|
+
Approve only when the trajectory shows a REAL trigger scenario and the
|
|
30
|
+
resulting skill would meet the standard; otherwise decline and say in the
|
|
31
|
+
rationale what must improve — drafting follows skill-creator, and the
|
|
32
|
+
planner receives the standard as its <skill_quality_standard> block.
|
|
33
|
+
|
|
34
|
+
Repeated multi-step workflows (session start/end routines, recurring
|
|
35
|
+
wrap-up or handoff procedures) are a valid refine target: approve with
|
|
36
|
+
instructions telling the planner to propose a guidance skill (kind=skill,
|
|
37
|
+
skill_kind=guidance — a SKILL.md document, no python reference). Only
|
|
38
|
+
propose when the same workflow recurs in the trajectory — never for
|
|
39
|
+
one-off flows. Auto-created skills are always offered to the user for a
|
|
40
|
+
decision before they land; the gate never writes a skill silently.
|
|
41
|
+
|
|
25
42
|
Return JSON only:
|
|
26
43
|
{
|
|
27
44
|
"shouldRefine": true|false,
|
package/lib/score.d.ts
CHANGED
|
@@ -9,12 +9,28 @@ export interface AggregateOptions {
|
|
|
9
9
|
passThreshold: number;
|
|
10
10
|
/** Per-case regression tolerance: candidate may drop below reference by at most this much. */
|
|
11
11
|
regressionTolerance: number;
|
|
12
|
+
/**
|
|
13
|
+
* Failure-cell protocol (gap A2): a round with more failed cells than
|
|
14
|
+
* this is rejected outright — failed cells are NEVER averaged in as
|
|
15
|
+
* zeros. Default 0 (any failure rejects the round).
|
|
16
|
+
*/
|
|
17
|
+
maxFailedCells: number;
|
|
12
18
|
}
|
|
13
19
|
export declare const DEFAULT_AGGREGATE: AggregateOptions;
|
|
14
|
-
|
|
15
|
-
export declare function aggregate(cells: readonly CellScore[]): Record<string, number | null> & {
|
|
20
|
+
export interface AggregateResult extends Record<string, number | null> {
|
|
16
21
|
overall: number | null;
|
|
17
|
-
|
|
22
|
+
/** Count of failed cells (excluded from every mean). */
|
|
23
|
+
failed: number;
|
|
24
|
+
/** Total cells considered (ok + failed). */
|
|
25
|
+
total: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Aggregate raw cells into code-owned per-case means + overall mean.
|
|
29
|
+
* Failure-cell protocol (gap A2): failed cells are EXCLUDED from means and
|
|
30
|
+
* counted separately — a crashed unit can never silently drag the mean down
|
|
31
|
+
* like a zero. A case whose cells all failed reports null (no mean).
|
|
32
|
+
*/
|
|
33
|
+
export declare function aggregate(cells: readonly CellScore[]): AggregateResult;
|
|
18
34
|
export declare function entryFromCells(label: string, cells: readonly CellScore[], refinementId?: string): EvaluationEntry;
|
|
19
35
|
export interface Decision {
|
|
20
36
|
accepted: boolean;
|
|
@@ -25,7 +41,9 @@ export declare function decisionReport(reference: EvaluationEntry, candidate: Ev
|
|
|
25
41
|
/**
|
|
26
42
|
* Non-regressive acceptance rule (Self-Harness style):
|
|
27
43
|
* the candidate is accepted iff its overall mean is STRICTLY higher than the
|
|
28
|
-
* reference
|
|
44
|
+
* reference, no case regresses by more than `regressionTolerance` points,
|
|
45
|
+
* and neither side has more failed cells than `maxFailedCells` (failure-cell
|
|
46
|
+
* protocol, gap A2 — a partial/invalid round is never accepted).
|
|
29
47
|
*/
|
|
30
48
|
export declare function decide(reference: EvaluationEntry, candidate: EvaluationEntry, opts: AggregateOptions): Decision;
|
|
31
49
|
//# sourceMappingURL=score.d.ts.map
|