dsh-continual-evolve 0.10.6 → 0.12.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 +35 -122
- package/README.zh.md +26 -97
- package/cordis.patch.yml +2 -2
- package/lib/index.d.ts +44 -241
- package/lib/index.js +30 -253
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +32 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/package.json +2 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -72
- package/lib/approval.js +0 -127
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command.d.ts +0 -49
- package/lib/command.js +0 -568
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -74
- package/lib/copy.js +0 -160
- package/lib/declines.d.ts +0 -101
- package/lib/declines.js +0 -175
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -59
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -196
- package/lib/inject.js +0 -412
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/memory-agent.d.ts +0 -129
- package/lib/memory-agent.js +0 -568
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -59
- package/lib/notify.js +0 -97
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -78
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -191
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -49
- package/lib/service.js +0 -180
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -62
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -72
- package/lib/turn-snapshot.js +0 -238
- package/lib/types.d.ts +0 -208
- package/lib/types.js +0 -127
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup.d.ts +0 -260
- package/lib/wrapup.js +0 -539
package/lib/score.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_AGGREGATE = {
|
|
2
|
-
passThreshold: 60,
|
|
3
|
-
regressionTolerance: 0,
|
|
4
|
-
maxFailedCells: 0,
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Aggregate raw cells into code-owned per-case means + overall mean.
|
|
8
|
-
* Failure-cell protocol (gap A2): failed cells are EXCLUDED from means and
|
|
9
|
-
* counted separately — a crashed unit can never silently drag the mean down
|
|
10
|
-
* like a zero. A case whose cells all failed reports null (no mean).
|
|
11
|
-
*/
|
|
12
|
-
export function aggregate(cells) {
|
|
13
|
-
const byCase = new Map();
|
|
14
|
-
let failed = 0;
|
|
15
|
-
for (const cell of cells) {
|
|
16
|
-
if (cell.status === "failed") {
|
|
17
|
-
failed += 1;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const list = byCase.get(cell.caseId) ?? [];
|
|
21
|
-
list.push(clampScore(cell.score));
|
|
22
|
-
byCase.set(cell.caseId, list);
|
|
23
|
-
}
|
|
24
|
-
const perCase = {};
|
|
25
|
-
for (const [caseId, scores] of byCase) {
|
|
26
|
-
perCase[caseId] = mean(scores);
|
|
27
|
-
}
|
|
28
|
-
const all = [...byCase.values()].flat();
|
|
29
|
-
let totalDurationMs = 0;
|
|
30
|
-
for (const cell of cells) {
|
|
31
|
-
if (cell.durationMs !== undefined && cell.durationMs >= 0) {
|
|
32
|
-
totalDurationMs += cell.durationMs;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
...perCase,
|
|
37
|
-
overall: all.length > 0 ? mean(all) : null,
|
|
38
|
-
failed,
|
|
39
|
-
total: cells.length,
|
|
40
|
-
totalDurationMs,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
export function entryFromCells(label, cells, refinementId) {
|
|
44
|
-
const aggr = aggregate(cells);
|
|
45
|
-
return {
|
|
46
|
-
label,
|
|
47
|
-
...(refinementId ? { refinementId } : {}),
|
|
48
|
-
createdAt: new Date().toISOString(),
|
|
49
|
-
cells: [...cells],
|
|
50
|
-
aggregate: aggr,
|
|
51
|
-
overall: aggr.overall,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** Human-readable decision report with per-case before → after deltas. */
|
|
55
|
-
export function decisionReport(reference, candidate, decision) {
|
|
56
|
-
const lines = [`overall: ${reference.overall ?? "?"} → ${candidate.overall ?? "?"}`];
|
|
57
|
-
// Per-case deltas over the actual evaluated cases only (aggregate() mixes
|
|
58
|
-
// case means with metadata keys like totalDurationMs — never render those
|
|
59
|
-
// as cases).
|
|
60
|
-
for (const caseId of new Set(reference.cells.map((cell) => cell.caseId))) {
|
|
61
|
-
const refScore = reference.aggregate[caseId];
|
|
62
|
-
if (refScore === null || refScore === undefined)
|
|
63
|
-
continue;
|
|
64
|
-
const candScore = candidate.aggregate[caseId];
|
|
65
|
-
const failedMark = isCaseFailed(reference, caseId) || isCaseFailed(candidate, caseId) ? " (failed)" : "";
|
|
66
|
-
lines.push(` ${caseId}: ${refScore} → ${candScore ?? "?"}${failedMark}`);
|
|
67
|
-
}
|
|
68
|
-
const refFailed = reference.aggregate.failed ?? 0;
|
|
69
|
-
const candFailed = candidate.aggregate.failed ?? 0;
|
|
70
|
-
if (refFailed > 0 || candFailed > 0) {
|
|
71
|
-
lines.push(`failed cells: reference ${refFailed}/${reference.aggregate.total ?? 0} · candidate ${candFailed}/${candidate.aggregate.total ?? 0}`);
|
|
72
|
-
}
|
|
73
|
-
// Gap C3: show duration summary when available.
|
|
74
|
-
const refDuration = reference.aggregate.totalDurationMs ?? 0;
|
|
75
|
-
const candDuration = candidate.aggregate.totalDurationMs ?? 0;
|
|
76
|
-
if (refDuration > 0 || candDuration > 0) {
|
|
77
|
-
lines.push(`duration: ${formatDuration(refDuration)} → ${formatDuration(candDuration)}`);
|
|
78
|
-
}
|
|
79
|
-
lines.push(decision.accepted
|
|
80
|
-
? "DECISION: ACCEPTED — overall improved, no regression"
|
|
81
|
-
: `DECISION: REJECTED — ${decision.reasons.join("; ")}`);
|
|
82
|
-
return lines;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Gap A3 (version_changed semantics): detect cells whose case material
|
|
86
|
-
* changed between the reference and candidate evaluation runs. A candidate
|
|
87
|
-
* cell whose `caseHash` differs from the reference cell of the SAME case
|
|
88
|
-
* means the statement/rubric was edited between the two runs — its score is
|
|
89
|
-
* not comparable to the baseline and must not count toward the decision.
|
|
90
|
-
*
|
|
91
|
-
* The check is conservative: cells without a hash on either side (pre-A3
|
|
92
|
-
* data) and cells already failed are left untouched. Mismatched cells are
|
|
93
|
-
* returned re-marked as `failed` with a reason in notes, so aggregation
|
|
94
|
-
* excludes them and the acceptance rule can reject the round.
|
|
95
|
-
*/
|
|
96
|
-
export function flagMaterialDrift(reference, candidateCells) {
|
|
97
|
-
const referenceHashes = new Map();
|
|
98
|
-
for (const cell of reference.cells) {
|
|
99
|
-
if (cell.status !== "failed" && cell.caseHash !== undefined && !referenceHashes.has(cell.caseId)) {
|
|
100
|
-
referenceHashes.set(cell.caseId, cell.caseHash);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (referenceHashes.size === 0) {
|
|
104
|
-
return [...candidateCells];
|
|
105
|
-
}
|
|
106
|
-
return candidateCells.map((cell) => {
|
|
107
|
-
if (cell.status === "failed" || cell.caseHash === undefined) {
|
|
108
|
-
return cell;
|
|
109
|
-
}
|
|
110
|
-
const refHash = referenceHashes.get(cell.caseId);
|
|
111
|
-
if (refHash !== undefined && refHash !== cell.caseHash) {
|
|
112
|
-
return {
|
|
113
|
-
...cell,
|
|
114
|
-
status: "failed",
|
|
115
|
-
passed: false,
|
|
116
|
-
notes: `materials changed: case ${cell.caseId} hash ${cell.caseHash} ≠ reference ${refHash} (re-run the reference or fix the material)`,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return cell;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
function isCaseFailed(entry, caseId) {
|
|
123
|
-
return entry.cells.some((cell) => cell.caseId === caseId && cell.status === "failed");
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Non-regressive acceptance rule (Self-Harness style):
|
|
127
|
-
* the candidate is accepted iff its overall mean is STRICTLY higher than the
|
|
128
|
-
* reference, no case regresses by more than `regressionTolerance` points,
|
|
129
|
-
* and neither side has more failed cells than `maxFailedCells` (failure-cell
|
|
130
|
-
* protocol, gap A2 — a partial/invalid round is never accepted).
|
|
131
|
-
*/
|
|
132
|
-
export function decide(reference, candidate, opts) {
|
|
133
|
-
const reasons = [];
|
|
134
|
-
if (reference.overall === null || candidate.overall === null) {
|
|
135
|
-
return { accepted: false, reasons: ["reference or candidate evaluation is incomplete"] };
|
|
136
|
-
}
|
|
137
|
-
const refFailed = reference.aggregate.failed ?? 0;
|
|
138
|
-
const candFailed = candidate.aggregate.failed ?? 0;
|
|
139
|
-
if (refFailed > opts.maxFailedCells) {
|
|
140
|
-
reasons.push(`reference has ${refFailed} failed cells (max ${opts.maxFailedCells})`);
|
|
141
|
-
}
|
|
142
|
-
if (candFailed > opts.maxFailedCells) {
|
|
143
|
-
reasons.push(`candidate has ${candFailed} failed cells (max ${opts.maxFailedCells})`);
|
|
144
|
-
}
|
|
145
|
-
if (reasons.length > 0) {
|
|
146
|
-
return { accepted: false, reasons };
|
|
147
|
-
}
|
|
148
|
-
if (candidate.overall <= reference.overall) {
|
|
149
|
-
reasons.push(`overall not improved: ${candidate.overall} <= ${reference.overall}`);
|
|
150
|
-
}
|
|
151
|
-
// Per-case regression is judged ONLY over the cases that actually exist in
|
|
152
|
-
// the evaluation — aggregate() mixes per-case means with metadata keys
|
|
153
|
-
// (failed/total/totalDurationMs), and iterating raw keys would (and did)
|
|
154
|
-
// treat totalDurationMs as a case score, rejecting a candidate whose run
|
|
155
|
-
// merely took longer. Derived from reference.cells, never from the
|
|
156
|
-
// aggregate key set.
|
|
157
|
-
for (const caseId of new Set(reference.cells.map((cell) => cell.caseId))) {
|
|
158
|
-
const refScore = reference.aggregate[caseId];
|
|
159
|
-
if (refScore === null || refScore === undefined) {
|
|
160
|
-
continue; // case had no comparable mean (all cells failed) — nothing to regress
|
|
161
|
-
}
|
|
162
|
-
const candScore = candidate.aggregate[caseId];
|
|
163
|
-
if (candScore === null || candScore === undefined) {
|
|
164
|
-
reasons.push(`candidate missing case ${caseId}`);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (candScore < refScore - opts.regressionTolerance) {
|
|
168
|
-
reasons.push(`case ${caseId} regressed: ${candScore} < ${refScore} - ${opts.regressionTolerance}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return { accepted: reasons.length === 0, reasons };
|
|
172
|
-
}
|
|
173
|
-
function mean(values) {
|
|
174
|
-
const sum = values.reduce((acc, value) => acc + value, 0);
|
|
175
|
-
return round2(sum / values.length);
|
|
176
|
-
}
|
|
177
|
-
function clampScore(score) {
|
|
178
|
-
if (!Number.isFinite(score))
|
|
179
|
-
return 0;
|
|
180
|
-
return Math.min(100, Math.max(0, score));
|
|
181
|
-
}
|
|
182
|
-
function round2(value) {
|
|
183
|
-
return Math.round(value * 100) / 100;
|
|
184
|
-
}
|
|
185
|
-
/** Gap C3: human-readable duration (ms → "1.2s" or "340ms"). */
|
|
186
|
-
function formatDuration(ms) {
|
|
187
|
-
if (ms < 1000)
|
|
188
|
-
return `${Math.round(ms)}ms`;
|
|
189
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
190
|
-
}
|
|
191
|
-
//# sourceMappingURL=score.js.map
|
package/lib/search.d.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Retrieval primitives for injection ranking: CJK-aware tokenization plus a
|
|
3
|
-
* dependency-free, field-weighted BM25 scorer (ADR
|
|
4
|
-
* `implemented/feature/2026-08-24-cjk-bigram-bm25-retrieval.md`).
|
|
5
|
-
*
|
|
6
|
-
* Why this shape:
|
|
7
|
-
* - the old scorer tokenized whole CJK runs as single tokens compared for
|
|
8
|
-
* exact equality, so any rewording of a Chinese query missed every entry
|
|
9
|
-
* (「检索升级」 never matched 「全文检索方案」); overlapping character
|
|
10
|
-
* bigrams — the standard cjk_bigram analyzer shape (Elasticsearch/
|
|
11
|
-
* OpenSearch) — fix recall while keeping English behavior identical;
|
|
12
|
-
* - raw hit counts weighted common words and distinctive terms alike; BM25's
|
|
13
|
-
* IDF separates them. We use Lucene's non-negative variant
|
|
14
|
-
* `ln(1 + (N - df + 0.5) / (df + 0.5))` so EVERY matched token scores > 0
|
|
15
|
-
* and unmatched documents score exactly 0, preserving rankEntries'
|
|
16
|
-
* "relevant outranks recent-but-irrelevant" ordering invariant;
|
|
17
|
-
* - the index lifetime is one ranking call. Stores hold tens~hundreds of
|
|
18
|
-
* entries (measured full rebuild + score: single-digit milliseconds), the
|
|
19
|
-
* JSON state files stay the single source of truth, and npm installers
|
|
20
|
-
* carry zero native/WASM baggage — better-sqlite3 FTS5, sql.js, MiniSearch,
|
|
21
|
-
* FlexSearch were all evaluated and rejected (see the ADR's alternatives).
|
|
22
|
-
*/
|
|
23
|
-
import type { HarnessEntry } from "./types.js";
|
|
24
|
-
/** BM25 term-saturation constant (Okapi k1). */
|
|
25
|
-
export declare const BM25_K1 = 1.5;
|
|
26
|
-
/** BM25 document-length normalization constant (Okapi b). */
|
|
27
|
-
export declare const BM25_B = 0.75;
|
|
28
|
-
/** Multiplier for title-field matches (title hits outweigh body hits 2:1). */
|
|
29
|
-
export declare const TITLE_FIELD_WEIGHT = 2;
|
|
30
|
-
/**
|
|
31
|
-
* Tokenize text for relevance scoring: lowercase ASCII runs become word
|
|
32
|
-
* tokens; CJK runs become overlapping character bigrams (a single-character
|
|
33
|
-
* run stays a unigram). Deterministic and allocation-light — safe to call per
|
|
34
|
-
* injection build.
|
|
35
|
-
*
|
|
36
|
-
* @param text Arbitrary entry or query text.
|
|
37
|
-
* @returns Tokens in occurrence order (duplicates kept; consumers dedupe when
|
|
38
|
-
* order-independent weighting is wanted).
|
|
39
|
-
*/
|
|
40
|
-
export declare function tokenize(text: string): string[];
|
|
41
|
-
/** Per-field corpus statistics for one ranking pass. */
|
|
42
|
-
interface FieldStats {
|
|
43
|
-
/** Term-frequency map per document, positioned like {@link RelevanceIndex.positions}. */
|
|
44
|
-
readonly tfs: ReadonlyArray<ReadonlyMap<string, number>>;
|
|
45
|
-
/** Token count per document (this field's dl for BM25 length normalization). */
|
|
46
|
-
readonly lengths: ReadonlyArray<number>;
|
|
47
|
-
/** Document frequency per distinct term across the corpus. */
|
|
48
|
-
readonly df: ReadonlyMap<string, number>;
|
|
49
|
-
/** Average token count across the corpus (0 when empty). */
|
|
50
|
-
readonly averageLength: number;
|
|
51
|
-
}
|
|
52
|
-
/** Precomputed corpus statistics backing {@link relevanceScore}. */
|
|
53
|
-
export interface RelevanceIndex {
|
|
54
|
-
/** Number of documents in the corpus. */
|
|
55
|
-
readonly size: number;
|
|
56
|
-
/** Identity positions: the exact entry objects the index was built from. */
|
|
57
|
-
readonly positions: ReadonlyMap<object, number>;
|
|
58
|
-
readonly titles: FieldStats;
|
|
59
|
-
readonly bodies: FieldStats;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Build the corpus statistics (per-field tf, df, average length) for one
|
|
63
|
-
* ranking call. Pure: the index reads its input snapshot and never mutates
|
|
64
|
-
* the entries.
|
|
65
|
-
*
|
|
66
|
-
* @param entries The candidate entries being ranked this call.
|
|
67
|
-
* @returns An index that answers {@link relevanceScore} for exactly these
|
|
68
|
-
* entries (identity-keyed).
|
|
69
|
-
*/
|
|
70
|
-
export declare function buildRelevanceIndex(entries: readonly HarnessEntry[]): RelevanceIndex;
|
|
71
|
-
/**
|
|
72
|
-
* Field-weighted BM25 score of `entry` against `query` within `index`:
|
|
73
|
-
* `TITLE_FIELD_WEIGHT × bm25(title) + bm25(body)`. Query tokens are deduped,
|
|
74
|
-
* so repeating a word adds no weight.
|
|
75
|
-
*
|
|
76
|
-
* @returns A score > 0 when at least one query token occurs in the entry
|
|
77
|
-
* (either field), and exactly 0 otherwise — the property rankEntries'
|
|
78
|
-
* relevance-first ordering relies on. Never throws; an entry outside
|
|
79
|
-
* the index simply scores 0.
|
|
80
|
-
*/
|
|
81
|
-
export declare function relevanceScore(index: RelevanceIndex, entry: HarnessEntry, query: string): number;
|
|
82
|
-
export {};
|
|
83
|
-
//# sourceMappingURL=search.d.ts.map
|
package/lib/search.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/** BM25 term-saturation constant (Okapi k1). */
|
|
2
|
-
export const BM25_K1 = 1.5;
|
|
3
|
-
/** BM25 document-length normalization constant (Okapi b). */
|
|
4
|
-
export const BM25_B = 0.75;
|
|
5
|
-
/** Multiplier for title-field matches (title hits outweigh body hits 2:1). */
|
|
6
|
-
export const TITLE_FIELD_WEIGHT = 2;
|
|
7
|
-
/**
|
|
8
|
-
* One pass, two run shapes: ASCII alphanumeric words, or maximal CJK runs.
|
|
9
|
-
* Runs never mix scripts because the alternation splits at the boundary, so
|
|
10
|
-
* 「深色主题dark主题」 tokenizes as 深色/色主/主题, "dark", 主题.
|
|
11
|
-
*/
|
|
12
|
-
const TOKEN_RUN_PATTERN = /[a-z0-9]+|[\u4e00-\u9fff]+/g;
|
|
13
|
-
const ASCII_RUN_PATTERN = /^[a-z0-9]+$/;
|
|
14
|
-
/**
|
|
15
|
-
* Tokenize text for relevance scoring: lowercase ASCII runs become word
|
|
16
|
-
* tokens; CJK runs become overlapping character bigrams (a single-character
|
|
17
|
-
* run stays a unigram). Deterministic and allocation-light — safe to call per
|
|
18
|
-
* injection build.
|
|
19
|
-
*
|
|
20
|
-
* @param text Arbitrary entry or query text.
|
|
21
|
-
* @returns Tokens in occurrence order (duplicates kept; consumers dedupe when
|
|
22
|
-
* order-independent weighting is wanted).
|
|
23
|
-
*/
|
|
24
|
-
export function tokenize(text) {
|
|
25
|
-
const tokens = [];
|
|
26
|
-
for (const match of text.toLowerCase().matchAll(TOKEN_RUN_PATTERN)) {
|
|
27
|
-
const run = match[0];
|
|
28
|
-
if (ASCII_RUN_PATTERN.test(run)) {
|
|
29
|
-
tokens.push(run);
|
|
30
|
-
}
|
|
31
|
-
else if (run.length === 1) {
|
|
32
|
-
tokens.push(run);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
for (let i = 0; i < run.length - 1; i += 1) {
|
|
36
|
-
tokens.push(run.slice(i, i + 2));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return tokens;
|
|
41
|
-
}
|
|
42
|
-
function buildFieldStats(fieldTokens) {
|
|
43
|
-
const tfs = fieldTokens.map((tokens) => {
|
|
44
|
-
const tf = new Map();
|
|
45
|
-
for (const token of tokens) {
|
|
46
|
-
tf.set(token, (tf.get(token) ?? 0) + 1);
|
|
47
|
-
}
|
|
48
|
-
return tf;
|
|
49
|
-
});
|
|
50
|
-
const df = new Map();
|
|
51
|
-
let totalLength = 0;
|
|
52
|
-
for (let position = 0; position < tfs.length; position += 1) {
|
|
53
|
-
const tf = tfs[position];
|
|
54
|
-
let length = 0;
|
|
55
|
-
for (const count of tf.values()) {
|
|
56
|
-
length += count;
|
|
57
|
-
}
|
|
58
|
-
totalLength += length;
|
|
59
|
-
for (const token of tf.keys()) {
|
|
60
|
-
df.set(token, (df.get(token) ?? 0) + 1);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
tfs,
|
|
65
|
-
lengths: fieldTokens.map((tokens) => tokens.length),
|
|
66
|
-
df,
|
|
67
|
-
averageLength: tfs.length === 0 ? 0 : totalLength / tfs.length,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Build the corpus statistics (per-field tf, df, average length) for one
|
|
72
|
-
* ranking call. Pure: the index reads its input snapshot and never mutates
|
|
73
|
-
* the entries.
|
|
74
|
-
*
|
|
75
|
-
* @param entries The candidate entries being ranked this call.
|
|
76
|
-
* @returns An index that answers {@link relevanceScore} for exactly these
|
|
77
|
-
* entries (identity-keyed).
|
|
78
|
-
*/
|
|
79
|
-
export function buildRelevanceIndex(entries) {
|
|
80
|
-
const titles = [];
|
|
81
|
-
const bodies = [];
|
|
82
|
-
for (const entry of entries) {
|
|
83
|
-
titles.push(tokenize(entry.title));
|
|
84
|
-
bodies.push(tokenize(`${entry.content} ${entry.path}`));
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
size: entries.length,
|
|
88
|
-
positions: new Map(entries.map((entry, position) => [entry, position])),
|
|
89
|
-
titles: buildFieldStats(titles),
|
|
90
|
-
bodies: buildFieldStats(bodies),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
/** One field's BM25 contribution for a single term (0 when the term is absent). */
|
|
94
|
-
function bm25Term(termFrequency, documentFrequency, corpusSize, docLength, averageLength) {
|
|
95
|
-
if (termFrequency <= 0 || documentFrequency <= 0) {
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
const inverseDocumentFrequency = Math.log(1 + (corpusSize - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
99
|
-
const normalization = averageLength > 0 ? 1 - BM25_B + BM25_B * (docLength / averageLength) : 1;
|
|
100
|
-
return inverseDocumentFrequency * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + BM25_K1 * normalization));
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Field-weighted BM25 score of `entry` against `query` within `index`:
|
|
104
|
-
* `TITLE_FIELD_WEIGHT × bm25(title) + bm25(body)`. Query tokens are deduped,
|
|
105
|
-
* so repeating a word adds no weight.
|
|
106
|
-
*
|
|
107
|
-
* @returns A score > 0 when at least one query token occurs in the entry
|
|
108
|
-
* (either field), and exactly 0 otherwise — the property rankEntries'
|
|
109
|
-
* relevance-first ordering relies on. Never throws; an entry outside
|
|
110
|
-
* the index simply scores 0.
|
|
111
|
-
*/
|
|
112
|
-
export function relevanceScore(index, entry, query) {
|
|
113
|
-
const position = index.positions.get(entry);
|
|
114
|
-
if (position === undefined || index.size === 0) {
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
117
|
-
const seen = new Set();
|
|
118
|
-
let titleScore = 0;
|
|
119
|
-
let bodyScore = 0;
|
|
120
|
-
for (const token of tokenize(query)) {
|
|
121
|
-
if (seen.has(token)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
seen.add(token);
|
|
125
|
-
const titleDf = index.titles.df.get(token) ?? 0;
|
|
126
|
-
if (titleDf > 0) {
|
|
127
|
-
titleScore += bm25Term(index.titles.tfs[position]?.get(token) ?? 0, titleDf, index.size, index.titles.lengths[position] ?? 0, index.titles.averageLength);
|
|
128
|
-
}
|
|
129
|
-
const bodyDf = index.bodies.df.get(token) ?? 0;
|
|
130
|
-
if (bodyDf > 0) {
|
|
131
|
-
bodyScore += bm25Term(index.bodies.tfs[position]?.get(token) ?? 0, bodyDf, index.size, index.bodies.lengths[position] ?? 0, index.bodies.averageLength);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return TITLE_FIELD_WEIGHT * titleScore + bodyScore;
|
|
135
|
-
}
|
|
136
|
-
//# sourceMappingURL=search.js.map
|
package/lib/service.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The evolution engine: the only entry point that mutates harness state.
|
|
3
|
-
* Every mutation path goes through here so snapshot-before-write, apply
|
|
4
|
-
* accounting, persistence, and result history are enforced in one place.
|
|
5
|
-
*/
|
|
6
|
-
import type { EntrySource, HarnessScope, RefinementProposal, RefinementResult } from "./types.js";
|
|
7
|
-
import { applyRefinementProposal } from "./apply.js";
|
|
8
|
-
import type { HistoryRetention } from "./store.js";
|
|
9
|
-
/** A post-commit apply failure that still carries the durable applied result. */
|
|
10
|
-
export declare class EvolutionApplyPostCommitError extends Error {
|
|
11
|
-
readonly result: RefinementResult;
|
|
12
|
-
readonly scope: HarnessScope;
|
|
13
|
-
readonly storeId: string | undefined;
|
|
14
|
-
readonly cause: unknown;
|
|
15
|
-
constructor(message: string, result: RefinementResult, scope: HarnessScope, storeId: string | undefined, cause: unknown);
|
|
16
|
-
}
|
|
17
|
-
export interface ApplyContext {
|
|
18
|
-
scope: HarnessScope;
|
|
19
|
-
sessionId?: string;
|
|
20
|
-
/** When set, optimistic-concurrency checks reject edits whose entries changed since this baseline. */
|
|
21
|
-
baselineState?: Parameters<typeof applyRefinementProposal>[0];
|
|
22
|
-
/** Trajectory citation stamped into newly created entries (see apply.ts). */
|
|
23
|
-
source?: EntrySource | undefined;
|
|
24
|
-
/** Marks the resulting refinement as the deterministic rollback of another (audit chain). */
|
|
25
|
-
rollbackOf?: string | undefined;
|
|
26
|
-
}
|
|
27
|
-
export interface EvolutionHooks {
|
|
28
|
-
/** Called after every applied refinement (side-effect boundary: skills sync, etc.). */
|
|
29
|
-
onApplied?: (result: RefinementResult) => void;
|
|
30
|
-
}
|
|
31
|
-
export interface EvolutionEngineOptions {
|
|
32
|
-
/**
|
|
33
|
-
* Storage-hygiene retention (#20): how many snapshots / JSONL tail
|
|
34
|
-
* lines each apply keeps. Resolved with generous defaults — rollback
|
|
35
|
-
* only needs recent snapshots and readers need a working window.
|
|
36
|
-
*/
|
|
37
|
-
historyRetain?: Partial<HistoryRetention>;
|
|
38
|
-
}
|
|
39
|
-
export declare function createEvolutionEngine(baseDir: string, hooks?: EvolutionHooks, opts?: EvolutionEngineOptions): {
|
|
40
|
-
load: (scope: HarnessScope, sessionId: string | undefined) => import("./types.js").HarnessState;
|
|
41
|
-
apply: (scope: HarnessScope, sessionId: string | undefined, proposal: RefinementProposal, context?: ApplyContext) => RefinementResult;
|
|
42
|
-
rollback: (scope: HarnessScope, sessionId: string | undefined, refinementId: string) => RefinementResult;
|
|
43
|
-
rollbackResult: (scope: HarnessScope, sessionId: string | undefined, target: RefinementResult) => RefinementResult;
|
|
44
|
-
history: (scope: HarnessScope, sessionId: string | undefined) => RefinementResult[];
|
|
45
|
-
baseDir: string;
|
|
46
|
-
retention: HistoryRetention;
|
|
47
|
-
};
|
|
48
|
-
export type EvolutionEngine = ReturnType<typeof createEvolutionEngine>;
|
|
49
|
-
//# sourceMappingURL=service.d.ts.map
|
package/lib/service.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { CONFLICT_HINT_KEY } from "./types.js";
|
|
2
|
-
import { applyRefinementProposal } from "./apply.js";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { rollbackProposal } from "./rollback.js";
|
|
5
|
-
import { loadHarnessState, saveHarnessState } from "./state.js";
|
|
6
|
-
import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
|
|
7
|
-
import { materializeMemoryProjection } from "./projection.js";
|
|
8
|
-
import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
|
|
9
|
-
/** A post-commit apply failure that still carries the durable applied result. */
|
|
10
|
-
export class EvolutionApplyPostCommitError extends Error {
|
|
11
|
-
result;
|
|
12
|
-
scope;
|
|
13
|
-
storeId;
|
|
14
|
-
cause;
|
|
15
|
-
constructor(message, result, scope, storeId, cause) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.result = result;
|
|
18
|
-
this.scope = scope;
|
|
19
|
-
this.storeId = storeId;
|
|
20
|
-
this.cause = cause;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
24
|
-
const retention = resolveHistoryRetention(opts.historyRetain);
|
|
25
|
-
/**
|
|
26
|
-
* Load a scope's state. For `project` the `sessionId` parameter carries
|
|
27
|
-
* the project key (see `resolveProjectKey` in project.ts); callers
|
|
28
|
-
* derive it with `projectKeyOf(agent)` and pass it through unchanged.
|
|
29
|
-
*/
|
|
30
|
-
function load(scope, sessionId) {
|
|
31
|
-
return loadHarnessState(storePaths(baseDir, scope, sessionId).stateDir, scope);
|
|
32
|
-
}
|
|
33
|
-
function apply(scope, sessionId, proposal, context) {
|
|
34
|
-
const paths = storePaths(baseDir, scope, sessionId);
|
|
35
|
-
// Working state is ALWAYS the freshest on-disk snapshot: apply mutates
|
|
36
|
-
// and persists the state whole-file, so building on the caller's
|
|
37
|
-
// planning-time copy would silently overwrite concurrent writers
|
|
38
|
-
// (another gate run, another session writing global). The caller's
|
|
39
|
-
// baselineState is ONLY the optimistic-concurrency comparison baseline
|
|
40
|
-
// — "reject edits whose target changed since planning" (review audit
|
|
41
|
-
// 2026-08-28 B1: the two roles were previously folded into one object,
|
|
42
|
-
// which made the advertised guard unreachable).
|
|
43
|
-
const state = load(scope, sessionId);
|
|
44
|
-
// Write-time conflict guard (R2): persistent-scope creates are
|
|
45
|
-
// checked against the existing same-kind entries BEFORE any side
|
|
46
|
-
// effect — a near-duplicate is rejected with an actionable error
|
|
47
|
-
// (evolve_update instead), a moderate overlap proceeds stamped with
|
|
48
|
-
// CONFLICT_HINT_KEY. Rollbacks bypass the guard: re-creating an entry
|
|
49
|
-
// that resembles its successor is the point of rollback. Local scope
|
|
50
|
-
// is never blocked (scratch space); the wrapup/fate promotion path
|
|
51
|
-
// already enforces its own overlap policy there.
|
|
52
|
-
//
|
|
53
|
-
// Secret-leak guard (P0, same throat): every persistent-scope create
|
|
54
|
-
// and update is screened before any side effect. Project and global
|
|
55
|
-
// stores both outlive the current session, so approval never turns a
|
|
56
|
-
// credential-shaped literal into an allowed write. The screen covers
|
|
57
|
-
// every field the edit can plant: title, content, and the JSON forms
|
|
58
|
-
// of reference, arguments, and metadata. Fixed patterns, not policy-
|
|
59
|
-
// configurable.
|
|
60
|
-
const warnHits = new Map();
|
|
61
|
-
if ((scope === "project" || scope === "global") && !context?.rollbackOf) {
|
|
62
|
-
for (const [index, edit] of proposal.edits.entries()) {
|
|
63
|
-
if (edit.action === "create" || edit.action === "update") {
|
|
64
|
-
const screenable = [
|
|
65
|
-
typeof edit.id === "string" ? edit.id : "",
|
|
66
|
-
typeof edit.path === "string" ? edit.path : "",
|
|
67
|
-
typeof edit.title === "string" ? edit.title : "",
|
|
68
|
-
typeof edit.content === "string" ? edit.content : "",
|
|
69
|
-
edit.reference !== undefined ? JSON.stringify(edit.reference) : "",
|
|
70
|
-
edit.arguments !== undefined ? JSON.stringify(edit.arguments) : "",
|
|
71
|
-
edit.metadata !== undefined ? JSON.stringify(edit.metadata) : "",
|
|
72
|
-
];
|
|
73
|
-
const secret = secretLeakReason(screenable.join("\n"));
|
|
74
|
-
if (secret) {
|
|
75
|
-
throw new Error(`${edit.action} blocked: ${secret}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (edit.action !== "create")
|
|
79
|
-
continue;
|
|
80
|
-
// An unknown kind must fail per-edit in validateEdit, never
|
|
81
|
-
// crash the whole proposal here (review audit 2026-08-28 S1).
|
|
82
|
-
const corpus = state.entries[edit.kind];
|
|
83
|
-
if (!corpus)
|
|
84
|
-
continue;
|
|
85
|
-
const hit = mostSimilarEntry(Object.values(corpus), edit.title ?? "", edit.content ?? "", CONFLICT_WARN_SCORE);
|
|
86
|
-
if (!hit)
|
|
87
|
-
continue;
|
|
88
|
-
if (hit.score >= CONFLICT_BLOCK_SCORE) {
|
|
89
|
-
throw new Error(`create blocked: ${buildConflictNotice(hit)} already lives in the global ${edit.kind} store — use evolve_update on it instead of adding a duplicate`);
|
|
90
|
-
}
|
|
91
|
-
warnHits.set(index, hit);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const id = `evolve_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
95
|
-
// Code-enforced snapshot: runs before any mutation, cannot be skipped by the model.
|
|
96
|
-
snapshotBefore(paths, id);
|
|
97
|
-
const result = applyRefinementProposal(state, proposal, {
|
|
98
|
-
id,
|
|
99
|
-
scope,
|
|
100
|
-
...(context?.source ? { source: context.source } : {}),
|
|
101
|
-
...(context?.baselineState ? { baselineState: context.baselineState } : {}),
|
|
102
|
-
...(context?.rollbackOf ? { rollbackOf: context.rollbackOf } : {}),
|
|
103
|
-
});
|
|
104
|
-
// Stamp warn-tier conflicts onto the freshly created entries (both the
|
|
105
|
-
// live state and the result's after-snapshot stay coherent).
|
|
106
|
-
for (const [index, hit] of warnHits) {
|
|
107
|
-
const applied = result.appliedEdits[index];
|
|
108
|
-
if (!applied?.applied || applied.action !== "create" || !applied.id)
|
|
109
|
-
continue;
|
|
110
|
-
const hint = `${applied.kind}:${hit.id}:${hit.score.toFixed(2)}`;
|
|
111
|
-
const live = state.entries[applied.kind][applied.id];
|
|
112
|
-
if (live) {
|
|
113
|
-
live.metadata[CONFLICT_HINT_KEY] = hint;
|
|
114
|
-
if (applied.after) {
|
|
115
|
-
applied.after.metadata[CONFLICT_HINT_KEY] = hint;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
saveHarnessState(paths.stateDir, state);
|
|
120
|
-
try {
|
|
121
|
-
appendResult(paths, result);
|
|
122
|
-
}
|
|
123
|
-
catch (cause) {
|
|
124
|
-
throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but failed to append history: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
|
|
125
|
-
}
|
|
126
|
-
// Storage hygiene (#20): bound the append-only past at write time —
|
|
127
|
-
// snapshots keep the newest N, the per-store history keeps its tail.
|
|
128
|
-
// Best-effort (never throws): a prune failure must not fail the apply.
|
|
129
|
-
try {
|
|
130
|
-
pruneSnapshots(paths.snapshotsDir, retention.snapshots);
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
// ignored — the next apply retries
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
pruneJsonlFile(paths.resultsPath, retention.refinements);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// ignored — the next apply retries
|
|
140
|
-
}
|
|
141
|
-
// Readable Markdown projection (P1): rewritten from the post-apply
|
|
142
|
-
// state whenever a memory edit landed, so rollback and archive stay
|
|
143
|
-
// in sync through this same apply path. The model has no writer for
|
|
144
|
-
// these files — only this engine entry point materializes them.
|
|
145
|
-
if (result.appliedEdits.some((edit) => edit.applied && edit.kind === "memory")) {
|
|
146
|
-
try {
|
|
147
|
-
materializeMemoryProjection(paths.stateDir, state);
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
// ignored — the projection is derived (JSON stays the source
|
|
151
|
-
// of truth) and the next memory apply retries
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
hooks.onApplied?.(result);
|
|
156
|
-
}
|
|
157
|
-
catch (cause) {
|
|
158
|
-
throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but a post-commit hook failed: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
/** Roll back an already-observed result without consulting history. */
|
|
163
|
-
function rollbackResult(scope, sessionId, target) {
|
|
164
|
-
return apply(scope, sessionId, rollbackProposal(target), { scope, rollbackOf: target.id });
|
|
165
|
-
}
|
|
166
|
-
function rollback(scope, sessionId, refinementId) {
|
|
167
|
-
const paths = storePaths(baseDir, scope, sessionId);
|
|
168
|
-
const history = loadResults(paths);
|
|
169
|
-
const target = history.find((item) => item.id === refinementId);
|
|
170
|
-
if (!target) {
|
|
171
|
-
throw new Error(`Refinement ${refinementId} not found in ${scope} history`);
|
|
172
|
-
}
|
|
173
|
-
return rollbackResult(scope, sessionId, target);
|
|
174
|
-
}
|
|
175
|
-
function history(scope, sessionId) {
|
|
176
|
-
return loadResults(storePaths(baseDir, scope, sessionId));
|
|
177
|
-
}
|
|
178
|
-
return { load, apply, rollback, rollbackResult, history, baseDir, retention };
|
|
179
|
-
}
|
|
180
|
-
//# sourceMappingURL=service.js.map
|
package/lib/skill-render.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill rendering: pure functions that convert harness entries into
|
|
3
|
-
* SKILL.md documents. Extracted from skill.ts to break the circular
|
|
4
|
-
* dependency between skill.ts ↔ skillquality.ts.
|
|
5
|
-
*
|
|
6
|
-
* Both skill.ts (materializer) and skillquality.ts (validator) need
|
|
7
|
-
* these rendering functions; importing from this shared leaf module
|
|
8
|
-
* keeps the dependency graph acyclic.
|
|
9
|
-
*/
|
|
10
|
-
import type { HarnessEntry } from "./types.js";
|
|
11
|
-
/** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
|
|
12
|
-
export declare function skillNameOf(id: string): string;
|
|
13
|
-
/**
|
|
14
|
-
* Render a harness skill entry as a discoverable SKILL.md document.
|
|
15
|
-
*
|
|
16
|
-
* 2026-08-22: the frontmatter description now carries a ROUTING HINT —
|
|
17
|
-
* title plus the first meaningful content line — instead of the bare title.
|
|
18
|
-
* The skill catalog matches on description; a title-only description gave
|
|
19
|
-
* loaders nothing to route on (observed: materialized skills were 7-line
|
|
20
|
-
* stubs with a one-line description and no use-when signal).
|
|
21
|
-
*/
|
|
22
|
-
export declare function renderSkillMarkdown(entry: HarnessEntry): string;
|
|
23
|
-
//# sourceMappingURL=skill-render.d.ts.map
|