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.
Files changed (126) hide show
  1. package/README.md +35 -122
  2. package/README.zh.md +26 -97
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -241
  5. package/lib/index.js +30 -253
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -72
  14. package/lib/approval.js +0 -127
  15. package/lib/auto.d.ts +0 -236
  16. package/lib/auto.js +0 -773
  17. package/lib/autocase.d.ts +0 -38
  18. package/lib/autocase.js +0 -76
  19. package/lib/benchmark-command.d.ts +0 -9
  20. package/lib/benchmark-command.js +0 -357
  21. package/lib/benchmark.d.ts +0 -151
  22. package/lib/benchmark.js +0 -250
  23. package/lib/command.d.ts +0 -49
  24. package/lib/command.js +0 -568
  25. package/lib/consolidate.d.ts +0 -90
  26. package/lib/consolidate.js +0 -180
  27. package/lib/copy.d.ts +0 -74
  28. package/lib/copy.js +0 -160
  29. package/lib/declines.d.ts +0 -101
  30. package/lib/declines.js +0 -175
  31. package/lib/evaluate.d.ts +0 -74
  32. package/lib/evaluate.js +0 -271
  33. package/lib/evolve-event.d.ts +0 -40
  34. package/lib/evolve-event.js +0 -59
  35. package/lib/failures.d.ts +0 -42
  36. package/lib/failures.js +0 -171
  37. package/lib/fate.d.ts +0 -130
  38. package/lib/fate.js +0 -350
  39. package/lib/goal-command.d.ts +0 -7
  40. package/lib/goal-command.js +0 -37
  41. package/lib/goal.d.ts +0 -72
  42. package/lib/goal.js +0 -72
  43. package/lib/inject.d.ts +0 -196
  44. package/lib/inject.js +0 -412
  45. package/lib/llm-text.d.ts +0 -104
  46. package/lib/llm-text.js +0 -140
  47. package/lib/logfile.d.ts +0 -71
  48. package/lib/logfile.js +0 -159
  49. package/lib/memory-agent.d.ts +0 -129
  50. package/lib/memory-agent.js +0 -568
  51. package/lib/memory-benchmark.d.ts +0 -76
  52. package/lib/memory-benchmark.js +0 -170
  53. package/lib/memory-index.d.ts +0 -114
  54. package/lib/memory-index.js +0 -197
  55. package/lib/message-source.d.ts +0 -23
  56. package/lib/message-source.js +0 -12
  57. package/lib/mount-command.d.ts +0 -10
  58. package/lib/mount-command.js +0 -48
  59. package/lib/mount.d.ts +0 -42
  60. package/lib/mount.js +0 -212
  61. package/lib/notify.d.ts +0 -59
  62. package/lib/notify.js +0 -97
  63. package/lib/plan.d.ts +0 -16
  64. package/lib/plan.js +0 -126
  65. package/lib/planner.d.ts +0 -61
  66. package/lib/planner.js +0 -144
  67. package/lib/pool.d.ts +0 -7
  68. package/lib/pool.js +0 -25
  69. package/lib/prefix-cache.d.ts +0 -78
  70. package/lib/prefix-cache.js +0 -168
  71. package/lib/project.d.ts +0 -28
  72. package/lib/project.js +0 -66
  73. package/lib/projection.d.ts +0 -22
  74. package/lib/projection.js +0 -112
  75. package/lib/promotion.d.ts +0 -94
  76. package/lib/promotion.js +0 -171
  77. package/lib/recall.d.ts +0 -85
  78. package/lib/recall.js +0 -142
  79. package/lib/record-language.d.ts +0 -85
  80. package/lib/record-language.js +0 -152
  81. package/lib/render.d.ts +0 -13
  82. package/lib/render.js +0 -80
  83. package/lib/review-scheduler.d.ts +0 -38
  84. package/lib/review-scheduler.js +0 -191
  85. package/lib/review.d.ts +0 -60
  86. package/lib/review.js +0 -145
  87. package/lib/rollback.d.ts +0 -9
  88. package/lib/rollback.js +0 -68
  89. package/lib/rubric.d.ts +0 -29
  90. package/lib/rubric.js +0 -119
  91. package/lib/runtime.d.ts +0 -24
  92. package/lib/runtime.js +0 -87
  93. package/lib/score.d.ts +0 -64
  94. package/lib/score.js +0 -191
  95. package/lib/search.d.ts +0 -83
  96. package/lib/search.js +0 -136
  97. package/lib/service.d.ts +0 -49
  98. package/lib/service.js +0 -180
  99. package/lib/skill-render.d.ts +0 -23
  100. package/lib/skill-render.js +0 -69
  101. package/lib/skill.d.ts +0 -15
  102. package/lib/skill.js +0 -80
  103. package/lib/skillquality.d.ts +0 -85
  104. package/lib/skillquality.js +0 -320
  105. package/lib/source.d.ts +0 -30
  106. package/lib/source.js +0 -43
  107. package/lib/state.d.ts +0 -26
  108. package/lib/state.js +0 -151
  109. package/lib/store.d.ts +0 -62
  110. package/lib/store.js +0 -164
  111. package/lib/token-usage.d.ts +0 -68
  112. package/lib/token-usage.js +0 -211
  113. package/lib/tool.d.ts +0 -34
  114. package/lib/tool.js +0 -320
  115. package/lib/turn-snapshot.d.ts +0 -72
  116. package/lib/turn-snapshot.js +0 -238
  117. package/lib/types.d.ts +0 -208
  118. package/lib/types.js +0 -127
  119. package/lib/usage.d.ts +0 -36
  120. package/lib/usage.js +0 -98
  121. package/lib/validate.d.ts +0 -28
  122. package/lib/validate.js +0 -181
  123. package/lib/wrapup-command.d.ts +0 -10
  124. package/lib/wrapup-command.js +0 -243
  125. package/lib/wrapup.d.ts +0 -260
  126. 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
@@ -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