@jmtrin/opencode-kevin 1.2.0 → 1.3.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 (141) hide show
  1. package/dist/plugin/index.d.ts +4 -3
  2. package/dist/plugin/index.js +122 -113
  3. package/dist/plugin/native.d.ts +2 -2
  4. package/dist/plugin/native.js +2 -2
  5. package/package.json +56 -68
  6. package/LICENSE +0 -21
  7. package/README.md +0 -605
  8. package/dist/migrations/001_initial.sql +0 -92
  9. package/dist/migrations/002_indexes.sql +0 -14
  10. package/dist/migrations/003_v02_signal.sql +0 -58
  11. package/dist/migrations/004_v03_knowledge.sql +0 -138
  12. package/dist/migrations/005_v04_signal.sql +0 -57
  13. package/dist/migrations/006_v05_glassbox.sql +0 -118
  14. package/dist/migrations/007_v06_pull.sql +0 -145
  15. package/dist/migrations/008_v07_truth.sql +0 -124
  16. package/dist/migrations/009_v08_team.sql +0 -100
  17. package/dist/migrations/010_v09_native.sql +0 -78
  18. package/dist/migrations/011_v10_proven.sql +0 -69
  19. package/dist/migrations/012_v11_drift.sql +0 -24
  20. package/dist/plugin/Archiver.d.ts +0 -42
  21. package/dist/plugin/Archiver.js +0 -83
  22. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  23. package/dist/plugin/ArtifactWriter.js +0 -291
  24. package/dist/plugin/CausalChain.d.ts +0 -22
  25. package/dist/plugin/CausalChain.js +0 -197
  26. package/dist/plugin/ChatBridge.d.ts +0 -41
  27. package/dist/plugin/ChatBridge.js +0 -103
  28. package/dist/plugin/ConflictDetector.d.ts +0 -35
  29. package/dist/plugin/ConflictDetector.js +0 -260
  30. package/dist/plugin/ContextInjector.d.ts +0 -222
  31. package/dist/plugin/ContextInjector.js +0 -533
  32. package/dist/plugin/ConventionMiner.d.ts +0 -35
  33. package/dist/plugin/ConventionMiner.js +0 -242
  34. package/dist/plugin/Curator.d.ts +0 -116
  35. package/dist/plugin/Curator.js +0 -324
  36. package/dist/plugin/DashboardHtml.d.ts +0 -5
  37. package/dist/plugin/DashboardHtml.js +0 -180
  38. package/dist/plugin/Feedback.d.ts +0 -67
  39. package/dist/plugin/Feedback.js +0 -120
  40. package/dist/plugin/HookLiveness.d.ts +0 -88
  41. package/dist/plugin/HookLiveness.js +0 -322
  42. package/dist/plugin/InjectionLedger.d.ts +0 -98
  43. package/dist/plugin/InjectionLedger.js +0 -295
  44. package/dist/plugin/LessonFixer.d.ts +0 -44
  45. package/dist/plugin/LessonFixer.js +0 -45
  46. package/dist/plugin/Materializer.d.ts +0 -83
  47. package/dist/plugin/Materializer.js +0 -181
  48. package/dist/plugin/MemoryService.d.ts +0 -360
  49. package/dist/plugin/MemoryService.js +0 -1242
  50. package/dist/plugin/Migrate.d.ts +0 -17
  51. package/dist/plugin/Migrate.js +0 -271
  52. package/dist/plugin/PatternMiner.d.ts +0 -49
  53. package/dist/plugin/PatternMiner.js +0 -132
  54. package/dist/plugin/QualityGate.d.ts +0 -124
  55. package/dist/plugin/QualityGate.js +0 -119
  56. package/dist/plugin/Reflector.d.ts +0 -91
  57. package/dist/plugin/Reflector.js +0 -307
  58. package/dist/plugin/RepoIdentity.d.ts +0 -124
  59. package/dist/plugin/RepoIdentity.js +0 -300
  60. package/dist/plugin/RepoTruth.d.ts +0 -80
  61. package/dist/plugin/RepoTruth.js +0 -599
  62. package/dist/plugin/Retrospective.d.ts +0 -17
  63. package/dist/plugin/Retrospective.js +0 -216
  64. package/dist/plugin/SharedLayer.d.ts +0 -159
  65. package/dist/plugin/SharedLayer.js +0 -462
  66. package/dist/plugin/Store.d.ts +0 -29
  67. package/dist/plugin/Store.js +0 -50
  68. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  69. package/dist/plugin/ToolCallObserver.js +0 -201
  70. package/dist/plugin/TuiActions.d.ts +0 -43
  71. package/dist/plugin/TuiActions.js +0 -181
  72. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  73. package/dist/plugin/TuiSnapshots.js +0 -158
  74. package/dist/plugin/columns.d.ts +0 -11
  75. package/dist/plugin/columns.js +0 -54
  76. package/dist/plugin/confidence.d.ts +0 -8
  77. package/dist/plugin/confidence.js +0 -34
  78. package/dist/plugin/contract.d.ts +0 -54
  79. package/dist/plugin/contract.js +0 -386
  80. package/dist/plugin/diff.d.ts +0 -8
  81. package/dist/plugin/diff.js +0 -182
  82. package/dist/plugin/escape.d.ts +0 -39
  83. package/dist/plugin/escape.js +0 -60
  84. package/dist/plugin/fingerprint.d.ts +0 -27
  85. package/dist/plugin/fingerprint.js +0 -73
  86. package/dist/plugin/inferability.d.ts +0 -32
  87. package/dist/plugin/inferability.js +0 -88
  88. package/dist/plugin/kevin_approve.d.ts +0 -34
  89. package/dist/plugin/kevin_approve.js +0 -52
  90. package/dist/plugin/kevin_audit.d.ts +0 -229
  91. package/dist/plugin/kevin_audit.js +0 -631
  92. package/dist/plugin/kevin_bench.d.ts +0 -10
  93. package/dist/plugin/kevin_bench.js +0 -82
  94. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  95. package/dist/plugin/kevin_conflicts.js +0 -50
  96. package/dist/plugin/kevin_contract.d.ts +0 -8
  97. package/dist/plugin/kevin_contract.js +0 -59
  98. package/dist/plugin/kevin_doctor.d.ts +0 -62
  99. package/dist/plugin/kevin_doctor.js +0 -196
  100. package/dist/plugin/kevin_facts.d.ts +0 -42
  101. package/dist/plugin/kevin_facts.js +0 -36
  102. package/dist/plugin/kevin_forget.d.ts +0 -33
  103. package/dist/plugin/kevin_forget.js +0 -260
  104. package/dist/plugin/kevin_native.d.ts +0 -29
  105. package/dist/plugin/kevin_native.js +0 -79
  106. package/dist/plugin/kevin_propose.d.ts +0 -23
  107. package/dist/plugin/kevin_propose.js +0 -14
  108. package/dist/plugin/kevin_publish.d.ts +0 -38
  109. package/dist/plugin/kevin_publish.js +0 -18
  110. package/dist/plugin/kevin_why.d.ts +0 -23
  111. package/dist/plugin/kevin_why.js +0 -111
  112. package/dist/plugin/memory-format.d.ts +0 -20
  113. package/dist/plugin/memory-format.js +0 -59
  114. package/dist/plugin/metrics.d.ts +0 -97
  115. package/dist/plugin/metrics.js +0 -288
  116. package/dist/plugin/okf-export.d.ts +0 -3
  117. package/dist/plugin/okf-export.js +0 -134
  118. package/dist/plugin/okf-import.d.ts +0 -76
  119. package/dist/plugin/okf-import.js +0 -271
  120. package/dist/plugin/okf.d.ts +0 -107
  121. package/dist/plugin/okf.js +0 -303
  122. package/dist/plugin/perf.d.ts +0 -35
  123. package/dist/plugin/perf.js +0 -174
  124. package/dist/plugin/query-tokenizer.d.ts +0 -13
  125. package/dist/plugin/query-tokenizer.js +0 -133
  126. package/dist/plugin/redact.d.ts +0 -2
  127. package/dist/plugin/redact.js +0 -18
  128. package/dist/plugin/replay-types.d.ts +0 -69
  129. package/dist/plugin/replay-types.js +0 -156
  130. package/dist/plugin/replay.d.ts +0 -36
  131. package/dist/plugin/replay.js +0 -202
  132. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  133. package/dist/plugin/sqlite-adapter.js +0 -81
  134. package/dist/plugin/time-ms.d.ts +0 -1
  135. package/dist/plugin/time-ms.js +0 -16
  136. package/dist/plugin/tui-types.d.ts +0 -59
  137. package/dist/plugin/tui-types.js +0 -4
  138. package/dist/plugin/tui.d.ts +0 -18
  139. package/dist/plugin/tui.js +0 -198
  140. package/dist/plugin/uuid.d.ts +0 -1
  141. package/dist/plugin/uuid.js +0 -50
@@ -1,17 +0,0 @@
1
- import type { Store } from "./Store.js";
2
- export interface MigrateResult {
3
- from: string;
4
- to: string;
5
- applied: string[];
6
- }
7
- export type PostApplyHook = (store: Store) => void;
8
- export declare const HOOK_NAMES: readonly ["tool.execute.before", "tool.execute.after", "chat.message", "experimental.chat.system.transform", "experimental.session.compacting", "event", "dispose"];
9
- export declare class Migrate {
10
- private store;
11
- private migrationsDir;
12
- private readonly postApplyHooks;
13
- constructor(store: Store, migrationsDir: string, postApplyHooks?: Record<string, PostApplyHook>);
14
- registerPostApply(version: string, hook: PostApplyHook): void;
15
- run(): Promise<MigrateResult>;
16
- private listPending;
17
- }
@@ -1,271 +0,0 @@
1
- import { readFileSync, readdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- // v0.9.0 (K9-001 / plan §5.3, D9-08) — the six hooks Kevin registers, in
4
- // hook-object order, plus `dispose` (v1.0.0 K10-013): the seventh
5
- // instrumented hook. Canonical list: the "010" post-apply hook seeds
6
- // hook_liveness from it, and HookLiveness (K9-009) wraps exactly these
7
- // names, so a future hook added here is visible in the table on the next
8
- // migration run.
9
- export const HOOK_NAMES = [
10
- "tool.execute.before",
11
- "tool.execute.after",
12
- "chat.message",
13
- "experimental.chat.system.transform",
14
- "experimental.session.compacting",
15
- "event",
16
- "dispose",
17
- ];
18
- // Built-in post-apply hooks, keyed by migration version. Each hook runs inside
19
- // the same transaction as the migration's DDL, so a hook failure rolls back the
20
- // whole migration. Hooks run when their version is applied. Migration 008's
21
- // reconciliation hook is also safe to run on later no-op starts (K7-002).
22
- const DEFAULT_POST_APPLY_HOOKS = {
23
- // v0.2.0 Signal Quality: defensive backfill of memories.origin for legacy
24
- // rows. The column is NOT NULL DEFAULT 'agent', so SQLite already populates
25
- // pre-existing rows with 'agent' on ALTER TABLE. This hook is a belt-and-
26
- // braces UPDATE that coerces any NULL/empty stragglers (which would only
27
- // exist if a partial DB skipped the DEFAULT) back to 'agent'.
28
- "003": (store) => {
29
- store
30
- .prepare("UPDATE memories SET origin = 'agent' WHERE origin IS NULL OR origin = ''")
31
- .run();
32
- },
33
- // v0.3.0 Knowledge + Causality: backfill evidence_count and status for
34
- // legacy rows. Columns have NOT NULL DEFAULT, so SQLite already populates
35
- // pre-existing rows. This hook is belt-and-braces in case a partial DB
36
- // skipped the defaults.
37
- "004": (store) => {
38
- store
39
- .prepare("UPDATE memories SET evidence_count = 0 WHERE evidence_count IS NULL")
40
- .run();
41
- store
42
- .prepare("UPDATE memories SET status = 'active' WHERE status IS NULL OR status = ''")
43
- .run();
44
- },
45
- // v0.4.0 Signal over Noise: backfill recurrence_count for legacy rows.
46
- // recurrence_count has NOT NULL DEFAULT 0, so SQLite already populates
47
- // pre-existing rows; fix_args and last_injected_at are nullable and need
48
- // no coercion. This hook is belt-and-braces in case a partial DB skipped
49
- // the defaults.
50
- "005": (store) => {
51
- store
52
- .prepare("UPDATE memories SET recurrence_count = 0 WHERE recurrence_count IS NULL")
53
- .run();
54
- },
55
- // v0.5.0 (K5-002 / plan §6, D5-13) — Glass Box: re-derive the four
56
- // injection counters from the ledger table instead of incrementing them.
57
- // The rebuild remapped prior `effective` rows to `inconclusive`, so any
58
- // pre-existing counter values would otherwise carry the v0.4 confound
59
- // forward. Re-derivation is idempotent by construction and self-healing
60
- // on earlier drift. No INSERTs: the rows are seeded by the migration SQL;
61
- // a missing row makes the UPDATE a harmless no-op.
62
- "006": (store) => {
63
- store
64
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections) WHERE key = 'injections_total'")
65
- .run();
66
- store
67
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'effective') WHERE key = 'injections_effective'")
68
- .run();
69
- store
70
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'ineffective') WHERE key = 'injections_ineffective'")
71
- .run();
72
- store
73
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'inconclusive') WHERE key = 'injections_inconclusive'")
74
- .run();
75
- },
76
- // v0.6.0 (K6-002 / plan §6, D6-08) — Pull: three re-derivations, all
77
- // idempotent by re-derivation (same discipline as "006", D5-13).
78
- // 1. Back-fill `inferable = 0` for the four non-`error` types still NULL:
79
- // they are `non_inferable` by rules 1 and 2 of plan §5.3
80
- // unconditionally. Guarded by `inferable IS NULL` so a re-run cannot
81
- // overwrite a classification produced later by inferability.classify().
82
- // `error` rows are left NULL and classified lazily.
83
- // 2-3. proposals_created / artifact_writes_total are re-derived from
84
- // their tables; a missing row makes the UPDATE a harmless no-op.
85
- "007": (store) => {
86
- store
87
- .prepare("UPDATE memories SET inferable = 0 WHERE inferable IS NULL AND type IN ('decision','rule','solution','pattern')")
88
- .run();
89
- store
90
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM curation_proposals) WHERE key = 'proposals_created'")
91
- .run();
92
- store
93
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM artifact_writes WHERE outcome = 'written') WHERE key = 'artifact_writes_total'")
94
- .run();
95
- },
96
- // v0.7.0 (K7-002 / plan §6, D7-02) — Truth: four re-derivations, all
97
- // idempotent by re-derivation (same discipline as "006"/"007", D5-13).
98
- // 1. Normalize any NULL truth_penalty to 0.0 (belt-and-braces; the column
99
- // is NOT NULL DEFAULT 0.0, so SQLite already back-fills on ALTER TABLE).
100
- // 2-4. repo_facts_scanned / conflicts_detected / memories_contradicted
101
- // are re-derived from their tables, NOT trusted from the counters, so
102
- // the metrics survive a database restored from backup or edited by
103
- // hand. A missing row makes the UPDATE a harmless no-op.
104
- "008": (store) => {
105
- store
106
- .prepare("UPDATE memories SET truth_penalty = 0.0 WHERE truth_penalty IS NULL")
107
- .run();
108
- store
109
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM repo_facts) WHERE key = 'repo_facts_scanned'")
110
- .run();
111
- store
112
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM memory_conflicts) WHERE key = 'conflicts_detected'")
113
- .run();
114
- store
115
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM memories WHERE truth_penalty > 0.0) WHERE key = 'memories_contradicted'")
116
- .run();
117
- },
118
- // v0.8.0 (K8-002 / plan §6.1, D8-03) — Team: three operations, all
119
- // idempotent by re-derivation (same discipline as "006"/"007"/"008").
120
- // 1. Back-fill the new scope from the old one: repo_id = project_id
121
- // keeps every pre-v0.8 row retrievable on the same machine with an
122
- // identical result set. Guarded by `repo_id IS NULL`; a row whose
123
- // project_id is also NULL keeps a NULL repo_id — the retrieval
124
- // path handles NULL as global (K8-007) instead of the hook faking
125
- // a value. No git-derived identity here: the hook runs inside a
126
- // migration and must not read the filesystem (K8-002 criterion).
127
- // 2. Normalize the layer marker for any row written by a concurrent
128
- // v0.7.0 process between ALTER and hook (belt and braces; the
129
- // column DEFAULT already covers the ordinary case).
130
- // 3. Re-derive shared_entries_total from state rather than trusting
131
- // an incremented value that may predate a crash.
132
- "009": (store) => {
133
- store
134
- .prepare("UPDATE memories SET repo_id = project_id WHERE repo_id IS NULL")
135
- .run();
136
- store
137
- .prepare("UPDATE memories SET layer = 'local' WHERE layer IS NULL OR layer = ''")
138
- .run();
139
- store
140
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM shared_entries) WHERE key = 'shared_entries_total'")
141
- .run();
142
- },
143
- // v0.9.0 (K9-001 / plan §6.1, D9-08) — Native: three operations, all
144
- // idempotent by design.
145
- // 1. Seed one hook_liveness row per name in HOOK_NAMES, with the
146
- // experimental flag derived from the hook's own `experimental.`
147
- // prefix and every counter at zero. Seeding eagerly (INSERT OR
148
- // IGNORE) makes a hook that has never fired a visible row with
149
- // fire_count = 0, not an absent row indistinguishable from a hook
150
- // Kevin does not register.
151
- // 2. Re-derive hooks_dead_total from hook_liveness state rather than
152
- // trusting an incremented value (same discipline as "006"-"009").
153
- // 3. Normalize any experimental flag that disagrees with its own
154
- // hook column's prefix — cheap, and it repairs a row hand-edited
155
- // during debugging.
156
- "010": (store) => {
157
- const seed = store.prepare("INSERT OR IGNORE INTO hook_liveness (hook, experimental) VALUES (?, ?)");
158
- for (const name of HOOK_NAMES) {
159
- seed.run(name, name.startsWith("experimental.") ? 1 : 0);
160
- }
161
- store
162
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM hook_liveness WHERE dead_since IS NOT NULL) WHERE key = 'hooks_dead_total'")
163
- .run();
164
- store
165
- .prepare("UPDATE hook_liveness SET experimental = CASE WHEN hook LIKE 'experimental.%' THEN 1 ELSE 0 END")
166
- .run();
167
- },
168
- // v1.0.0 (K10-005 / plan §6.1) — Proven: four operations, all idempotent.
169
- // 1. Seed dispose row defensively.
170
- // 2-3. Re-derive perf_budget_breaches and bench_runs_total.
171
- // 4. Normalise NULL within_budget to 1.
172
- "011": (store) => {
173
- store
174
- .prepare("INSERT OR IGNORE INTO hook_liveness (hook) VALUES ('dispose')")
175
- .run();
176
- store
177
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM perf_samples WHERE within_budget = 0) WHERE key = 'perf_budget_breaches'")
178
- .run();
179
- store
180
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM bench_runs) WHERE key = 'bench_runs_total'")
181
- .run();
182
- store
183
- .prepare("UPDATE perf_samples SET within_budget = 1 WHERE within_budget IS NULL")
184
- .run();
185
- },
186
- };
187
- export class Migrate {
188
- store;
189
- migrationsDir;
190
- postApplyHooks;
191
- constructor(store, migrationsDir, postApplyHooks) {
192
- this.store = store;
193
- this.migrationsDir = migrationsDir;
194
- this.postApplyHooks = new Map(Object.entries({
195
- ...DEFAULT_POST_APPLY_HOOKS,
196
- ...(postApplyHooks ?? {}),
197
- }));
198
- }
199
- registerPostApply(version, hook) {
200
- this.postApplyHooks.set(version, hook);
201
- }
202
- async run() {
203
- this.store.exec(`CREATE TABLE IF NOT EXISTS schema_version (
204
- version TEXT PRIMARY KEY,
205
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
206
- );`);
207
- const currentRow = this.store
208
- .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
209
- .get();
210
- const from = currentRow?.version ?? "000";
211
- const pending = this.listPending(from);
212
- if (pending.length === 0) {
213
- // v0.7.0 (K7-002) — heal drift in the 008 counters on a no-op
214
- // startup while preserving `applied: []` idempotency. v0.8.0
215
- // (K8-002 / plan §6.1) extends the same repair to the 009
216
- // back-fill and the shared_entries_total re-derivation, and
217
- // v0.9.0 (K9-001 / plan §6.1) to the 010 seeding,
218
- // hooks_dead_total re-derivation and experimental
219
- // normalization: all three hooks are idempotent by guarded
220
- // updates and re-derivation, so a no-op startup can heal a
221
- // crash that landed between the DDL and the hook without
222
- // re-applying DDL.
223
- if (from === "008" ||
224
- from === "009" ||
225
- from === "010" ||
226
- from === "011") {
227
- const repairHook = this.postApplyHooks.get(from);
228
- if (repairHook)
229
- this.store.transaction(() => repairHook(this.store));
230
- }
231
- return { from, to: from, applied: [] };
232
- }
233
- const insertVersion = this.store.prepare("INSERT OR IGNORE INTO schema_version (version) VALUES (?)");
234
- for (const migration of pending) {
235
- const sql = readFileSync(join(this.migrationsDir, migration.file), "utf8");
236
- this.store.transaction(() => {
237
- this.store.exec(sql);
238
- const hook = this.postApplyHooks.get(migration.version);
239
- if (hook)
240
- hook(this.store);
241
- insertVersion.run(migration.version);
242
- });
243
- }
244
- return {
245
- from,
246
- to: pending[pending.length - 1].version,
247
- applied: pending.map((m) => m.version),
248
- };
249
- }
250
- // v1.1.0 (K11-015) — lexicographic ordering is valid through "999" because
251
- // versions are zero-padded 3-digit strings ("001" … "999"). Any future
252
- // migration beyond 999 must use a 4-digit prefix and this comparison must
253
- // become numeric (parseInt). Until then, string > works and keeps the
254
- // migration idempotency simple (plan §5.5, D11-??).
255
- listPending(current) {
256
- let files = [];
257
- try {
258
- files = readdirSync(this.migrationsDir).filter((f) => f.endsWith(".sql"));
259
- }
260
- catch {
261
- return [];
262
- }
263
- files.sort();
264
- return files
265
- .map((file) => {
266
- const match = file.match(/^(\w+?)_/);
267
- return match ? { version: match[1], file } : null;
268
- })
269
- .filter((m) => m !== null && m.version > current);
270
- }
271
- }
@@ -1,49 +0,0 @@
1
- import type { MemoryService } from "./MemoryService.js";
2
- import type { Store } from "./Store.js";
3
- import type { Metrics } from "./metrics.js";
4
- /**
5
- * v0.2.0 PatternMiner (K2-021, plan §B6.10 / D2-08).
6
- *
7
- * Opt-in deterministic pattern miner. Reads recent `tool_calls` for a given
8
- * project, groups consecutive ordered 2-grams of `(tool_name)` and 3-grams
9
- * where the *middle* tool was a failure, and when a group reaches the
10
- * threshold (default `N ≥ 5` distinct sessions), emits a single
11
- * `type='pattern'`, `origin='pattern'` memory describing the pattern.
12
- *
13
- * Default OFF — must be enabled by setting `kevin_settings.patternminer_enabled`
14
- * to `'1'`. Idempotent via SELECT-before-INSERT keyed on
15
- * `(project_id, fingerprint, type='pattern', origin='pattern')`. Migration 003's
16
- * partial UNIQUE index only covers `type='error' AND origin='reflector'`, so
17
- * pattern memories cannot rely on a database uniqueness constraint — the
18
- * SELECT check inside `mine()` is the single idempotency mechanism.
19
- *
20
- * NO LLM hop (D2-08). The emitted suggestion is a deterministic template built
21
- * from the captured tool names.
22
- */
23
- export interface PatternMinerOptions {
24
- /** Minimum distinct sessions a pattern must appear in before emission.
25
- * Default 5 (D2-08). */
26
- threshold?: number;
27
- }
28
- export declare class PatternMiner {
29
- private readonly store;
30
- private readonly memoryService;
31
- private readonly metrics;
32
- private readonly threshold;
33
- constructor(store: Store, memoryService: MemoryService, metrics?: Metrics | null, options?: PatternMinerOptions);
34
- /**
35
- * Mine patterns observed in `tool_calls` for the given project. Returns the
36
- * number of NEW `pattern` memories emitted this cycle.
37
- *
38
- * When the opt-in flag `kevin_settings.patternminer_enabled` is not set to
39
- * `'1'` (default), this is a no-op and returns 0.
40
- *
41
- * When `projectId` is null/undefined, mines tool_calls whose `project_id`
42
- * IS NULL (legacy / opt-out flow). When `projectId` is a string, mines
43
- * tool_calls scoped to that project only.
44
- */
45
- mine(projectId?: string | null): number;
46
- private isEnabled;
47
- private fetchToolCalls;
48
- private collectCandidates;
49
- }
@@ -1,132 +0,0 @@
1
- import { fingerprint as computeFingerprint } from "./fingerprint.js";
2
- const DEFAULT_THRESHOLD = 5;
3
- const SETTING_KEY = "patternminer_enabled";
4
- export class PatternMiner {
5
- store;
6
- memoryService;
7
- metrics;
8
- threshold;
9
- constructor(store, memoryService, metrics, options) {
10
- this.store = store;
11
- this.memoryService = memoryService;
12
- this.metrics = metrics ?? null;
13
- this.threshold = options?.threshold ?? DEFAULT_THRESHOLD;
14
- }
15
- /**
16
- * Mine patterns observed in `tool_calls` for the given project. Returns the
17
- * number of NEW `pattern` memories emitted this cycle.
18
- *
19
- * When the opt-in flag `kevin_settings.patternminer_enabled` is not set to
20
- * `'1'` (default), this is a no-op and returns 0.
21
- *
22
- * When `projectId` is null/undefined, mines tool_calls whose `project_id`
23
- * IS NULL (legacy / opt-out flow). When `projectId` is a string, mines
24
- * tool_calls scoped to that project only.
25
- */
26
- mine(projectId) {
27
- if (!this.isEnabled())
28
- return 0;
29
- const rows = this.fetchToolCalls(projectId ?? null);
30
- if (rows.length === 0)
31
- return 0;
32
- const candidates = this.collectCandidates(rows);
33
- if (candidates.length === 0)
34
- return 0;
35
- let emitted = 0;
36
- for (const c of candidates) {
37
- if (c.sessions.size < this.threshold)
38
- continue;
39
- const fp = computeFingerprint(c.content, projectId ?? undefined);
40
- // Idempotency: migration 003's partial unique only covers
41
- // (type='error', origin='reflector'). For pattern memories we
42
- // SELECT to detect a prior emission with the same
43
- // (project_id, fingerprint, type='pattern', origin='pattern').
44
- const existing = this.store
45
- .prepare(`SELECT id FROM memories
46
- WHERE type = 'pattern' AND origin = 'pattern'
47
- AND fingerprint = ?
48
- AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))
49
- LIMIT 1`)
50
- .get(fp, projectId ?? null, projectId ?? null);
51
- if (existing)
52
- continue;
53
- this.memoryService.save({
54
- type: "pattern",
55
- origin: "pattern",
56
- fingerprint: fp,
57
- content: c.content,
58
- scope: "project",
59
- projectId: projectId ?? undefined,
60
- relevanceScore: 0.5,
61
- sourceTool: "PatternMiner",
62
- });
63
- this.metrics?.incr("patterns_mined", 1);
64
- emitted += 1;
65
- }
66
- return emitted;
67
- }
68
- isEnabled() {
69
- const row = this.store
70
- .prepare("SELECT value FROM kevin_settings WHERE key = ?")
71
- .get(SETTING_KEY);
72
- return row?.value === "1";
73
- }
74
- fetchToolCalls(projectId) {
75
- const nullPid = projectId === null || projectId === undefined;
76
- const sql = nullPid
77
- ? `SELECT id, session_id, ts, tool, success FROM tool_calls
78
- WHERE project_id IS NULL
79
- ORDER BY session_id ASC, ts ASC`
80
- : `SELECT id, session_id, ts, tool, success FROM tool_calls
81
- WHERE project_id = ?
82
- ORDER BY session_id ASC, ts ASC`;
83
- const stmt = this.store.prepare(sql);
84
- const rows = (nullPid ? stmt.all() : stmt.all(projectId));
85
- return rows;
86
- }
87
- collectCandidates(rows) {
88
- // Group rows by session_id preserving arrival order. The SQL already
89
- // orders by (session_id ASC, ts ASC), so a sequential scan yields each
90
- // session's tool_calls in execution order.
91
- const bySession = new Map();
92
- for (const r of rows) {
93
- let list = bySession.get(r.session_id);
94
- if (!list) {
95
- list = [];
96
- bySession.set(r.session_id, list);
97
- }
98
- list.push(r);
99
- }
100
- // 2-grams: ordered pair (a, b) of consecutive tool names per session.
101
- // 3-grams: ordered triple (a, b, c) where the middle tool b failed
102
- // (success = 0). These capture the "X→Y fails then Z" lifecycle that
103
- // the plan §B6.10 calls out.
104
- const map = new Map();
105
- const record = (key, content, sessionId) => {
106
- let cand = map.get(key);
107
- if (!cand) {
108
- cand = { key, content, sessions: new Set() };
109
- map.set(key, cand);
110
- }
111
- cand.sessions.add(sessionId);
112
- };
113
- for (const [sessionId, list] of bySession) {
114
- for (let i = 0; i < list.length - 1; i++) {
115
- const a = list[i];
116
- const b = list[i + 1];
117
- const key2 = `2g::${a.tool}::${b.tool}`;
118
- const content2 = `Pattern: tool "${a.tool}" followed by tool "${b.tool}". Review the ${a.tool}→${b.tool} contract before retrying.`;
119
- record(key2, content2, sessionId);
120
- if (i + 2 < list.length) {
121
- const c = list[i + 2];
122
- if (b.success === 0) {
123
- const key3 = `3g::${a.tool}::${b.tool}::${c.tool}`;
124
- const content3 = `Pattern: tool "${a.tool}" followed by failing tool "${b.tool}" then tool "${c.tool}". Review the ${a.tool}→${b.tool}(failed)→${c.tool} contract before retrying.`;
125
- record(key3, content3, sessionId);
126
- }
127
- }
128
- }
129
- }
130
- return Array.from(map.values());
131
- }
132
- }
@@ -1,124 +0,0 @@
1
- import type { DispatchedLesson } from "./Reflector.js";
2
- /**
3
- * v0.4.0 (K4-003/K4-004 / plan §5.1) — QualityGate.
4
- *
5
- * Pure, NO-LLM predicates deciding whether a lesson is worth injecting into
6
- * the model's prompt. Principle 11: never inject noise.
7
- *
8
- * - `evaluate` — classifies a lesson (rescued errorType, non-generic
9
- * suggestion, isActionable, strength).
10
- * - `rescueErrorType` — a dispatched code overrides the coarse
11
- * `errorType` (e.g. `'unknown'` → `'TS2304'`), fixing the defect where
12
- * lessons said "fails with unknown" even when the Reflector DID identify
13
- * the error code.
14
- * - `canInject` — per-session + per-fingerprint admission rules (K4-004).
15
- */
16
- /** The v0.1.x fallback suggestions. A lesson whose suggestion is one of these
17
- * (and has no dispatched code) carries no actionable information. */
18
- export declare const GENERIC_SUGGESTIONS: ReadonlySet<string>;
19
- export declare function isGenericSuggestion(suggestion: string): boolean;
20
- export interface LessonQuality {
21
- /** Rescued errorType: the dispatched code when one matched, else the
22
- * coarse errorType (may be `'unknown'`). */
23
- errorType: string;
24
- /** The lesson's suggestion text (rule-produced or generic fallback). */
25
- suggestion: string;
26
- /** True when the lesson carries actionable information: a dispatched
27
- * code exists OR the suggestion is not one of the generic fallbacks. */
28
- isActionable: boolean;
29
- /** `strong` when a code matched or the rescued errorType is not
30
- * `'unknown'`; `weak` otherwise. */
31
- strength: "strong" | "weak";
32
- }
33
- export interface QualityLesson {
34
- errorType: string;
35
- suggestion: string;
36
- }
37
- export interface InjectionContext {
38
- /** Memory ids already injected in the current session. */
39
- seenThisSession: Set<string>;
40
- /** Failing tool_calls count for this fingerprint in the current session
41
- * (0 = never recurred after injection). */
42
- recurrenceCount: number;
43
- /** v0.6.0 (K6-022 / plan §5.8) — the `injection_confidence_floor`
44
- * setting, read once per plan/inject call by the ContextInjector.
45
- * When present, a memory whose `confidence` is below it is rejected by
46
- * the zero branch (before the seen-set). Undefined disables the branch:
47
- * memories without a computed confidence are never dropped by it. */
48
- confidenceFloor?: number;
49
- }
50
- /**
51
- * v0.5.0 (K5-006 / plan §5.2, D5-04) — the reason a memory was rejected
52
- * (or admitted). Every rejection reason maps 1:1 to an
53
- * `injections_blocked_*` counter; a silent boolean is an unmeasurable
54
- * policy (principle 16).
55
- */
56
- export type GateReason = "ok" | "low_confidence" | "seen_this_session" | "ignored" | "not_active" | "recurrence" | "weak";
57
- export interface GateVerdict {
58
- readonly allowed: boolean;
59
- readonly reason: GateReason;
60
- }
61
- export declare const QualityGate: {
62
- /**
63
- * A dispatched code always overrides the coarse errorType for display:
64
- * `'unknown'` + `TS2304` → `'TS2304'`.
65
- */
66
- rescueErrorType(dispatch: DispatchedLesson | null, errorType: string): string;
67
- /** Classify a lesson per §5.1 rule 1 (rescue) and the generic ban. */
68
- evaluate(lesson: QualityLesson, dispatch: DispatchedLesson | null, errorType: string): LessonQuality;
69
- /**
70
- * v0.4.0 (K4-004 / plan §5.1 rules 2-4) — admission gate evaluated at
71
- * injection time. A lesson is injectable only when:
72
- *
73
- * 1. its memory id was not already injected this session (seen-set),
74
- * 2. its memory is not `stale`,
75
- * 3. its fingerprint has not recurred since injection (`recurrenceCount
76
- * === 0` — a lesson that failed to prevent the error leaves the
77
- * prompt),
78
- * 4. it is actionable AND strong — weak lessons with generic suggestions
79
- * are never injected (unless `qualityGateEnabled = false` for debug,
80
- * K4-023).
81
- *
82
- * v0.5.0 (K5-006 / plan §5.2, D5-04) — the boolean form is a thin
83
- * wrapper over `canInjectVerdict`; the reason is never discarded by
84
- * callers that use the verdict form.
85
- */
86
- canInject(memory: {
87
- id: string;
88
- status?: string;
89
- strength?: "strong" | "weak";
90
- isActionable?: boolean;
91
- }, ctx: InjectionContext, qualityGateEnabled?: boolean): boolean;
92
- /**
93
- * v0.5.0 (K5-006 / plan §5.2, D5-04) — the verdict form of `canInject`.
94
- * Branch order preserves the v0.4.0 sequence with `ignored` inserted
95
- * second:
96
- *
97
- * 0. `confidence < ctx.confidenceFloor` → `low_confidence` (v0.6.0,
98
- * K6-022 — runs FIRST: it is the cheapest check, and a memory below
99
- * the floor should not consume a seen-set slot it will never use)
100
- * 1. seen this session → `seen_this_session`
101
- * 2. `memory.ignored === true` → `ignored`
102
- * 3. status !== 'active' → `not_active`
103
- * 4. recurrence in session → `recurrence`
104
- * 5. weak / not actionable → `weak` (skipped entirely when
105
- * `qualityGateEnabled === false` — the debug flag bypasses the
106
- * quality check only, never the seen/ignored/status/recurrence bans)
107
- *
108
- * `ignored` is a human verdict (D5-07) and is enforced even in debug
109
- * mode: the flag excludes a memory from retrieval entirely, so the gate
110
- * is only ever asked about it defensively.
111
- *
112
- * The floor comparison is `>=`-admissive: a memory exactly AT the floor
113
- * passes the branch (asserted in K6-022). A floor of '0' admits
114
- * everything the other five branches allow.
115
- */
116
- canInjectVerdict(memory: {
117
- id: string;
118
- status?: string;
119
- strength?: "strong" | "weak";
120
- isActionable?: boolean;
121
- ignored?: boolean;
122
- confidence?: number;
123
- }, ctx: InjectionContext, qualityGateEnabled?: boolean): GateVerdict;
124
- };