dsh-continual-evolve 0.11.0 → 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 (154) hide show
  1. package/README.md +35 -115
  2. package/README.zh.md +26 -93
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -225
  5. package/lib/index.js +30 -239
  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 -73
  14. package/lib/approval.js +0 -127
  15. package/lib/audit-log.d.ts +0 -19
  16. package/lib/audit-log.js +0 -52
  17. package/lib/auto.d.ts +0 -236
  18. package/lib/auto.js +0 -773
  19. package/lib/autocase.d.ts +0 -38
  20. package/lib/autocase.js +0 -76
  21. package/lib/benchmark-command.d.ts +0 -9
  22. package/lib/benchmark-command.js +0 -357
  23. package/lib/benchmark.d.ts +0 -151
  24. package/lib/benchmark.js +0 -250
  25. package/lib/command-util.d.ts +0 -70
  26. package/lib/command-util.js +0 -123
  27. package/lib/command.d.ts +0 -13
  28. package/lib/command.js +0 -134
  29. package/lib/consolidate.d.ts +0 -90
  30. package/lib/consolidate.js +0 -180
  31. package/lib/copy.d.ts +0 -37
  32. package/lib/copy.js +0 -58
  33. package/lib/declines.d.ts +0 -103
  34. package/lib/declines.js +0 -179
  35. package/lib/evaluate.d.ts +0 -74
  36. package/lib/evaluate.js +0 -271
  37. package/lib/evolve-event.d.ts +0 -40
  38. package/lib/evolve-event.js +0 -60
  39. package/lib/extraction-phase.d.ts +0 -132
  40. package/lib/extraction-phase.js +0 -236
  41. package/lib/failures.d.ts +0 -42
  42. package/lib/failures.js +0 -171
  43. package/lib/fate.d.ts +0 -130
  44. package/lib/fate.js +0 -350
  45. package/lib/goal-command.d.ts +0 -7
  46. package/lib/goal-command.js +0 -37
  47. package/lib/goal.d.ts +0 -72
  48. package/lib/goal.js +0 -72
  49. package/lib/inject.d.ts +0 -90
  50. package/lib/inject.js +0 -164
  51. package/lib/injection-caps.d.ts +0 -4
  52. package/lib/injection-caps.js +0 -4
  53. package/lib/injection-directory.d.ts +0 -29
  54. package/lib/injection-directory.js +0 -78
  55. package/lib/injection-rank.d.ts +0 -85
  56. package/lib/injection-rank.js +0 -161
  57. package/lib/listener.d.ts +0 -45
  58. package/lib/listener.js +0 -199
  59. package/lib/llm-text.d.ts +0 -104
  60. package/lib/llm-text.js +0 -140
  61. package/lib/logfile.d.ts +0 -71
  62. package/lib/logfile.js +0 -159
  63. package/lib/maintenance-commands.d.ts +0 -23
  64. package/lib/maintenance-commands.js +0 -136
  65. package/lib/memory-agent.d.ts +0 -93
  66. package/lib/memory-agent.js +0 -300
  67. package/lib/memory-apply.d.ts +0 -44
  68. package/lib/memory-apply.js +0 -202
  69. package/lib/memory-benchmark.d.ts +0 -76
  70. package/lib/memory-benchmark.js +0 -170
  71. package/lib/memory-index.d.ts +0 -114
  72. package/lib/memory-index.js +0 -197
  73. package/lib/memory-manifest.d.ts +0 -16
  74. package/lib/memory-manifest.js +0 -70
  75. package/lib/message-source.d.ts +0 -23
  76. package/lib/message-source.js +0 -12
  77. package/lib/mount-command.d.ts +0 -10
  78. package/lib/mount-command.js +0 -48
  79. package/lib/mount.d.ts +0 -42
  80. package/lib/mount.js +0 -212
  81. package/lib/notify.d.ts +0 -48
  82. package/lib/notify.js +0 -57
  83. package/lib/plan-command.d.ts +0 -12
  84. package/lib/plan-command.js +0 -38
  85. package/lib/plan.d.ts +0 -16
  86. package/lib/plan.js +0 -126
  87. package/lib/planner.d.ts +0 -61
  88. package/lib/planner.js +0 -144
  89. package/lib/pool.d.ts +0 -7
  90. package/lib/pool.js +0 -25
  91. package/lib/prefix-cache.d.ts +0 -79
  92. package/lib/prefix-cache.js +0 -168
  93. package/lib/project.d.ts +0 -28
  94. package/lib/project.js +0 -66
  95. package/lib/projection.d.ts +0 -22
  96. package/lib/projection.js +0 -112
  97. package/lib/promotion.d.ts +0 -94
  98. package/lib/promotion.js +0 -171
  99. package/lib/recall.d.ts +0 -85
  100. package/lib/recall.js +0 -142
  101. package/lib/record-language.d.ts +0 -85
  102. package/lib/record-language.js +0 -152
  103. package/lib/render.d.ts +0 -13
  104. package/lib/render.js +0 -80
  105. package/lib/review-scheduler.d.ts +0 -38
  106. package/lib/review-scheduler.js +0 -183
  107. package/lib/review.d.ts +0 -60
  108. package/lib/review.js +0 -145
  109. package/lib/rollback.d.ts +0 -9
  110. package/lib/rollback.js +0 -68
  111. package/lib/rubric.d.ts +0 -29
  112. package/lib/rubric.js +0 -119
  113. package/lib/runtime-commands.d.ts +0 -14
  114. package/lib/runtime-commands.js +0 -115
  115. package/lib/runtime.d.ts +0 -24
  116. package/lib/runtime.js +0 -87
  117. package/lib/score.d.ts +0 -64
  118. package/lib/score.js +0 -191
  119. package/lib/search.d.ts +0 -83
  120. package/lib/search.js +0 -136
  121. package/lib/service.d.ts +0 -63
  122. package/lib/service.js +0 -253
  123. package/lib/skill-render.d.ts +0 -23
  124. package/lib/skill-render.js +0 -69
  125. package/lib/skill.d.ts +0 -15
  126. package/lib/skill.js +0 -80
  127. package/lib/skillquality.d.ts +0 -85
  128. package/lib/skillquality.js +0 -320
  129. package/lib/source.d.ts +0 -30
  130. package/lib/source.js +0 -43
  131. package/lib/state.d.ts +0 -26
  132. package/lib/state.js +0 -151
  133. package/lib/store.d.ts +0 -61
  134. package/lib/store.js +0 -164
  135. package/lib/token-usage.d.ts +0 -68
  136. package/lib/token-usage.js +0 -211
  137. package/lib/tool.d.ts +0 -34
  138. package/lib/tool.js +0 -320
  139. package/lib/turn-snapshot.d.ts +0 -78
  140. package/lib/turn-snapshot.js +0 -252
  141. package/lib/types.d.ts +0 -218
  142. package/lib/types.js +0 -133
  143. package/lib/usage.d.ts +0 -36
  144. package/lib/usage.js +0 -98
  145. package/lib/validate.d.ts +0 -28
  146. package/lib/validate.js +0 -181
  147. package/lib/wrapup-candidates.d.ts +0 -127
  148. package/lib/wrapup-candidates.js +0 -161
  149. package/lib/wrapup-command.d.ts +0 -10
  150. package/lib/wrapup-command.js +0 -243
  151. package/lib/wrapup-proposals.d.ts +0 -101
  152. package/lib/wrapup-proposals.js +0 -281
  153. package/lib/wrapup.d.ts +0 -56
  154. package/lib/wrapup.js +0 -105
@@ -1,115 +0,0 @@
1
- import { loadUsage, getUsageCount } from "./usage.js";
2
- import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
3
- import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
4
- import { success } from "./command-util.js";
5
- /** `/evolve pause|resume` — the runtime switch is independent of registration. */
6
- export function executePauseResumeCommand(engine, sub, runtime) {
7
- const pausing = sub === "pause";
8
- const defaultEnabled = runtime.autoReview ?? false;
9
- const current = loadGateRuntime(engine.baseDir, defaultEnabled);
10
- if (pausing && current.paused) {
11
- return success("automatic Memory Agent is already paused (no change).");
12
- }
13
- if (!pausing && current.enabled && !current.paused) {
14
- return success("automatic Memory Agent is already running (no change).");
15
- }
16
- saveGateRuntime(engine.baseDir, pausing, pausing ? current.enabled : true);
17
- return success(pausing
18
- ? "automatic Memory Agent paused: no automatic memory extraction calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
19
- : "automatic Memory Agent resumed: extraction moments (compaction / goal-blocked / session-close drain) are live again.");
20
- }
21
- /** `/evolve status` — listener wiring plus per-store entry counts and retention. */
22
- export function executeStatusCommand(engine, sessionId, projectKey, runtime) {
23
- const configuredDefault = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
24
- const current = loadGateRuntime(engine.baseDir, runtime.autoReview ?? false);
25
- const effective = current.paused
26
- ? "PAUSED (resume with /evolve resume)"
27
- : current.enabled
28
- ? "Memory Agent running"
29
- : "Memory Agent off (enable with /evolve resume)";
30
- const gateLine = `gate: moment-driven Memory Agent listener registered · config default ${configuredDefault} · runtime ${effective}`;
31
- const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
32
- const lines = [gateLine];
33
- lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
34
- if (projectKey) {
35
- try {
36
- lines[lines.length - 1] += ` · project ${countEntries(engine.load("project", projectKey))} entries`;
37
- }
38
- catch {
39
- lines[lines.length - 1] += " · project (unavailable)";
40
- }
41
- }
42
- const retention = engine.retention;
43
- if (retention) {
44
- lines.push(`retention: snapshots ${retention.snapshots} · refinements ${retention.refinements} · reviews ${retention.reviews} · token usage ${retention.tokenUsage} (historyRetain)`);
45
- }
46
- return success(lines.join("\n"));
47
- }
48
- /**
49
- * `/evolve usage` — injection counts per entry across the stores the human
50
- * can see (global + this session + this project), plus the exact direct-call
51
- * token report. Counts are per-session since the v2 usage shape — "in how
52
- * many sessions did this entry surface".
53
- */
54
- export function executeUsageCommand(engine, sessionId, projectKey) {
55
- const store = loadUsage(engine.baseDir);
56
- const rows = [];
57
- const seen = new Set();
58
- const collect = (state) => {
59
- for (const kind of Object.keys(state.entries)) {
60
- for (const entry of Object.values(state.entries[kind])) {
61
- const key = `${kind}:${entry.id}`;
62
- if (seen.has(key))
63
- continue;
64
- seen.add(key);
65
- const count = getUsageCount(store, kind, entry.id);
66
- rows.push({
67
- key,
68
- title: entry.title,
69
- count,
70
- ...(store.lastSession?.[`${kind}:${entry.id}`] ? { lastSession: store.lastSession[`${kind}:${entry.id}`] } : {}),
71
- });
72
- }
73
- }
74
- };
75
- collect(engine.load("global", undefined));
76
- collect(engine.load("local", sessionId));
77
- if (projectKey) {
78
- try {
79
- collect(engine.load("project", projectKey));
80
- }
81
- catch {
82
- // project store unavailable here — global+local still report
83
- }
84
- }
85
- const liveKeys = new Set(rows.map((r) => r.key));
86
- const orphaned = Object.keys(store.counts).filter((k) => !liveKeys.has(k)).length;
87
- const total = rows.reduce((n, r) => n + r.count, 0);
88
- const injected = rows.filter((r) => r.count > 0).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
89
- const stale = rows.filter((r) => r.count === 0).sort((a, b) => (a.key < b.key ? -1 : 1));
90
- const lines = [`injected entries: ${total} injections across ${injected.length} of ${rows.length} stored entries${orphaned > 0 ? ` (+${orphaned} historical key(s) for deleted entries)` : ""}`];
91
- if (injected.length > 0) {
92
- lines.push("injected (top 15):");
93
- for (const row of injected.slice(0, 15)) {
94
- lines.push(` ${row.key} — ${row.count}× · ${row.title}${row.lastSession ? ` (last in ${row.lastSession})` : ""}`);
95
- }
96
- if (injected.length > 15) {
97
- lines.push(` … and ${injected.length - 15} more`);
98
- }
99
- }
100
- else {
101
- lines.push("injected: (none yet — nothing has surfaced into a session prompt)");
102
- }
103
- if (stale.length > 0) {
104
- lines.push(`never injected (${stale.length}):`);
105
- for (const row of stale.slice(0, 20)) {
106
- lines.push(` ${row.key} · ${row.title}`);
107
- }
108
- if (stale.length > 20) {
109
- lines.push(` … and ${stale.length - 20} more`);
110
- }
111
- }
112
- lines.push("", ...renderTokenUsageReport(loadTokenUsage(engine.baseDir), engine.retention.tokenUsage));
113
- return success(lines.join("\n"));
114
- }
115
- //# sourceMappingURL=runtime-commands.js.map
package/lib/runtime.d.ts DELETED
@@ -1,24 +0,0 @@
1
- /** On-disk runtime gate state. Version 2 adds an explicit enable bit. */
2
- export interface GateRuntime {
3
- version: 2;
4
- /** Whether the human has enabled automatic review. */
5
- enabled: boolean;
6
- /** Whether the human has explicitly paused automatic review. */
7
- paused: boolean;
8
- /** ISO timestamp of the last pause/resume write. */
9
- updatedAt: string;
10
- }
11
- /** Full path of the runtime switch file. */
12
- export declare function runtimePath(baseDir: string): string;
13
- /**
14
- * Load the runtime switch. Missing, malformed, or legacy content uses the
15
- * supplied configured default and never throws; this is a trigger-path read.
16
- */
17
- export declare function loadGateRuntime(baseDir: string, defaultEnabled?: boolean): GateRuntime;
18
- /** Persist the runtime switch atomically (temp file + rename). */
19
- export declare function saveGateRuntime(baseDir: string, paused: boolean, enabled?: boolean): string;
20
- /** True while the human paused the automatic review gate. */
21
- export declare function isGatePaused(baseDir: string): boolean;
22
- /** Effective automatic-review state, including the explicit enable bit. */
23
- export declare function isGateEnabled(baseDir: string, defaultEnabled?: boolean): boolean;
24
- //# sourceMappingURL=runtime.d.ts.map
package/lib/runtime.js DELETED
@@ -1,87 +0,0 @@
1
- /**
2
- * Runtime gate switch: a small human-owned file that enables or pauses the
3
- * automatic review listener without editing the profile or restarting DSH.
4
- *
5
- * - file: `<baseDir>/evolve/runtime.json` (`{version: 2, enabled, paused, updatedAt}`)
6
- * - the file is consulted at every trigger and before scheduler execution;
7
- * - `/evolve pause` sets `paused`, `/evolve resume` sets `enabled=true` and
8
- * clears the pause, while manual tools and commands remain independent;
9
- * - a missing or malformed file uses the caller's configured default. A
10
- * legacy v1 `{paused}` file is readable and preserves its pause state.
11
- */
12
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
13
- import { randomUUID } from "node:crypto";
14
- import { join } from "node:path";
15
- import { EVOLVE_DIR } from "./store.js";
16
- const RUNTIME_FILE = "runtime.json";
17
- /** Full path of the runtime switch file. */
18
- export function runtimePath(baseDir) {
19
- return join(baseDir, EVOLVE_DIR, RUNTIME_FILE);
20
- }
21
- function state(enabled, paused) {
22
- return { version: 2, enabled, paused, updatedAt: new Date(0).toISOString() };
23
- }
24
- /**
25
- * Load the runtime switch. Missing, malformed, or legacy content uses the
26
- * supplied configured default and never throws; this is a trigger-path read.
27
- */
28
- export function loadGateRuntime(baseDir, defaultEnabled = false) {
29
- const path = runtimePath(baseDir);
30
- if (!existsSync(path))
31
- return state(defaultEnabled, false);
32
- try {
33
- const raw = JSON.parse(readFileSync(path, "utf8"));
34
- if (typeof raw !== "object" || raw === null || Array.isArray(raw))
35
- return state(defaultEnabled, false);
36
- const record = raw;
37
- const paused = record["paused"] === true;
38
- // v1 had no enable bit. Its only meaningful state was pause, so retain
39
- // the configured default when it is not paused.
40
- const enabled = typeof record["enabled"] === "boolean" ? record["enabled"] : defaultEnabled;
41
- return {
42
- version: 2,
43
- enabled,
44
- paused,
45
- updatedAt: typeof record["updatedAt"] === "string" ? record["updatedAt"] : new Date(0).toISOString(),
46
- };
47
- }
48
- catch {
49
- return state(defaultEnabled, false);
50
- }
51
- }
52
- /** Persist the runtime switch atomically (temp file + rename). */
53
- export function saveGateRuntime(baseDir, paused, enabled = true) {
54
- const path = runtimePath(baseDir);
55
- mkdirSync(join(baseDir, EVOLVE_DIR), { recursive: true });
56
- const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
57
- const payload = { version: 2, enabled, paused, updatedAt: new Date().toISOString() };
58
- writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
59
- try {
60
- renameSync(tempPath, path);
61
- }
62
- finally {
63
- if (existsSync(tempPath))
64
- unlinkSync(tempPath);
65
- }
66
- return path;
67
- }
68
- /** True while the human paused the automatic review gate. */
69
- export function isGatePaused(baseDir) {
70
- try {
71
- return loadGateRuntime(baseDir).paused;
72
- }
73
- catch {
74
- return false;
75
- }
76
- }
77
- /** Effective automatic-review state, including the explicit enable bit. */
78
- export function isGateEnabled(baseDir, defaultEnabled = false) {
79
- try {
80
- const current = loadGateRuntime(baseDir, defaultEnabled);
81
- return current.enabled && !current.paused;
82
- }
83
- catch {
84
- return defaultEnabled;
85
- }
86
- }
87
- //# sourceMappingURL=runtime.js.map
package/lib/score.d.ts DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * Code-owned scoring: aggregation and acceptance decisions. The model only
3
- * produces per-cell raw scores; every average and every accept/reject call
4
- * happens here, in deterministic code.
5
- */
6
- import type { CellScore, EvaluationEntry } from "./benchmark.js";
7
- export interface AggregateOptions {
8
- /** A cell is "passed" if its raw score is at least this threshold. */
9
- passThreshold: number;
10
- /** Per-case regression tolerance: candidate may drop below reference by at most this much. */
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;
18
- }
19
- export declare const DEFAULT_AGGREGATE: AggregateOptions;
20
- export interface AggregateResult extends Record<string, number | null> {
21
- overall: number | null;
22
- /** Count of failed cells (excluded from every mean). */
23
- failed: number;
24
- /** Total cells considered (ok + failed). */
25
- total: number;
26
- /** Gap C3: total wall-clock duration of all cells in milliseconds. */
27
- totalDurationMs: number;
28
- }
29
- /**
30
- * Aggregate raw cells into code-owned per-case means + overall mean.
31
- * Failure-cell protocol (gap A2): failed cells are EXCLUDED from means and
32
- * counted separately — a crashed unit can never silently drag the mean down
33
- * like a zero. A case whose cells all failed reports null (no mean).
34
- */
35
- export declare function aggregate(cells: readonly CellScore[]): AggregateResult;
36
- export declare function entryFromCells(label: string, cells: readonly CellScore[], refinementId?: string): EvaluationEntry;
37
- export interface Decision {
38
- accepted: boolean;
39
- reasons: string[];
40
- }
41
- /** Human-readable decision report with per-case before → after deltas. */
42
- export declare function decisionReport(reference: EvaluationEntry, candidate: EvaluationEntry, decision: Decision): string[];
43
- /**
44
- * Gap A3 (version_changed semantics): detect cells whose case material
45
- * changed between the reference and candidate evaluation runs. A candidate
46
- * cell whose `caseHash` differs from the reference cell of the SAME case
47
- * means the statement/rubric was edited between the two runs — its score is
48
- * not comparable to the baseline and must not count toward the decision.
49
- *
50
- * The check is conservative: cells without a hash on either side (pre-A3
51
- * data) and cells already failed are left untouched. Mismatched cells are
52
- * returned re-marked as `failed` with a reason in notes, so aggregation
53
- * excludes them and the acceptance rule can reject the round.
54
- */
55
- export declare function flagMaterialDrift(reference: EvaluationEntry, candidateCells: readonly CellScore[]): CellScore[];
56
- /**
57
- * Non-regressive acceptance rule (Self-Harness style):
58
- * the candidate is accepted iff its overall mean is STRICTLY higher than the
59
- * reference, no case regresses by more than `regressionTolerance` points,
60
- * and neither side has more failed cells than `maxFailedCells` (failure-cell
61
- * protocol, gap A2 — a partial/invalid round is never accepted).
62
- */
63
- export declare function decide(reference: EvaluationEntry, candidate: EvaluationEntry, opts: AggregateOptions): Decision;
64
- //# sourceMappingURL=score.d.ts.map
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