@jmtrin/kevin-core 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 (145) hide show
  1. package/dist/Archiver.d.ts +42 -0
  2. package/dist/Archiver.js +83 -0
  3. package/dist/ArtifactWriter.d.ts +93 -0
  4. package/dist/ArtifactWriter.js +291 -0
  5. package/dist/CausalChain.d.ts +22 -0
  6. package/dist/CausalChain.js +197 -0
  7. package/dist/ChatBridge.d.ts +41 -0
  8. package/dist/ChatBridge.js +103 -0
  9. package/dist/ConflictDetector.d.ts +35 -0
  10. package/dist/ConflictDetector.js +260 -0
  11. package/dist/ContextInjector.d.ts +222 -0
  12. package/dist/ContextInjector.js +533 -0
  13. package/dist/ConventionMiner.d.ts +35 -0
  14. package/dist/ConventionMiner.js +242 -0
  15. package/dist/Curator.d.ts +118 -0
  16. package/dist/Curator.js +326 -0
  17. package/dist/DashboardHtml.d.ts +5 -0
  18. package/dist/DashboardHtml.js +180 -0
  19. package/dist/Feedback.d.ts +67 -0
  20. package/dist/Feedback.js +120 -0
  21. package/dist/HookLiveness.d.ts +88 -0
  22. package/dist/HookLiveness.js +322 -0
  23. package/dist/InjectionLedger.d.ts +98 -0
  24. package/dist/InjectionLedger.js +295 -0
  25. package/dist/LessonFixer.d.ts +44 -0
  26. package/dist/LessonFixer.js +45 -0
  27. package/dist/Materializer.d.ts +84 -0
  28. package/dist/Materializer.js +181 -0
  29. package/dist/MemoryService.d.ts +360 -0
  30. package/dist/MemoryService.js +1242 -0
  31. package/dist/Migrate.d.ts +24 -0
  32. package/dist/Migrate.js +285 -0
  33. package/dist/PatternMiner.d.ts +49 -0
  34. package/dist/PatternMiner.js +132 -0
  35. package/dist/QualityGate.d.ts +124 -0
  36. package/dist/QualityGate.js +119 -0
  37. package/dist/Reflector.d.ts +91 -0
  38. package/dist/Reflector.js +307 -0
  39. package/dist/RepoIdentity.d.ts +124 -0
  40. package/dist/RepoIdentity.js +300 -0
  41. package/dist/RepoTruth.d.ts +80 -0
  42. package/dist/RepoTruth.js +599 -0
  43. package/dist/Retrospective.d.ts +18 -0
  44. package/dist/Retrospective.js +216 -0
  45. package/dist/SharedLayer.d.ts +159 -0
  46. package/dist/SharedLayer.js +462 -0
  47. package/dist/Store.d.ts +29 -0
  48. package/dist/Store.js +50 -0
  49. package/dist/ToolCallObserver.d.ts +32 -0
  50. package/dist/ToolCallObserver.js +201 -0
  51. package/dist/TuiActions.d.ts +43 -0
  52. package/dist/TuiActions.js +181 -0
  53. package/dist/TuiSnapshots.d.ts +24 -0
  54. package/dist/TuiSnapshots.js +159 -0
  55. package/dist/capabilities.d.ts +6 -0
  56. package/dist/capabilities.js +1 -0
  57. package/dist/columns.d.ts +11 -0
  58. package/dist/columns.js +54 -0
  59. package/dist/confidence.d.ts +8 -0
  60. package/dist/confidence.js +34 -0
  61. package/dist/contract.d.ts +68 -0
  62. package/dist/contract.js +430 -0
  63. package/dist/diff.d.ts +8 -0
  64. package/dist/diff.js +182 -0
  65. package/dist/env.d.ts +5 -0
  66. package/dist/env.js +12 -0
  67. package/dist/escape.d.ts +39 -0
  68. package/dist/escape.js +60 -0
  69. package/dist/fingerprint.d.ts +27 -0
  70. package/dist/fingerprint.js +73 -0
  71. package/dist/host.d.ts +18 -0
  72. package/dist/host.js +7 -0
  73. package/dist/idle-pipeline.d.ts +9 -0
  74. package/dist/idle-pipeline.js +44 -0
  75. package/dist/index.d.ts +72 -0
  76. package/dist/index.js +109 -0
  77. package/dist/inferability.d.ts +32 -0
  78. package/dist/inferability.js +88 -0
  79. package/dist/kevin_approve.d.ts +34 -0
  80. package/dist/kevin_approve.js +52 -0
  81. package/dist/kevin_audit.d.ts +230 -0
  82. package/dist/kevin_audit.js +631 -0
  83. package/dist/kevin_bench.d.ts +11 -0
  84. package/dist/kevin_bench.js +83 -0
  85. package/dist/kevin_conflicts.d.ts +9 -0
  86. package/dist/kevin_conflicts.js +50 -0
  87. package/dist/kevin_contract.d.ts +8 -0
  88. package/dist/kevin_contract.js +59 -0
  89. package/dist/kevin_doctor.d.ts +63 -0
  90. package/dist/kevin_doctor.js +197 -0
  91. package/dist/kevin_facts.d.ts +42 -0
  92. package/dist/kevin_facts.js +36 -0
  93. package/dist/kevin_forget.d.ts +33 -0
  94. package/dist/kevin_forget.js +260 -0
  95. package/dist/kevin_native.d.ts +29 -0
  96. package/dist/kevin_native.js +79 -0
  97. package/dist/kevin_propose.d.ts +23 -0
  98. package/dist/kevin_propose.js +14 -0
  99. package/dist/kevin_publish.d.ts +38 -0
  100. package/dist/kevin_publish.js +18 -0
  101. package/dist/kevin_why.d.ts +23 -0
  102. package/dist/kevin_why.js +111 -0
  103. package/dist/memory-format.d.ts +20 -0
  104. package/dist/memory-format.js +59 -0
  105. package/dist/metrics.d.ts +97 -0
  106. package/dist/metrics.js +288 -0
  107. package/dist/migrations/001_initial.sql +92 -0
  108. package/dist/migrations/002_indexes.sql +14 -0
  109. package/dist/migrations/003_v02_signal.sql +58 -0
  110. package/dist/migrations/004_v03_knowledge.sql +138 -0
  111. package/dist/migrations/005_v04_signal.sql +57 -0
  112. package/dist/migrations/006_v05_glassbox.sql +118 -0
  113. package/dist/migrations/007_v06_pull.sql +145 -0
  114. package/dist/migrations/008_v07_truth.sql +124 -0
  115. package/dist/migrations/009_v08_team.sql +100 -0
  116. package/dist/migrations/010_v09_native.sql +78 -0
  117. package/dist/migrations/011_v10_proven.sql +69 -0
  118. package/dist/migrations/012_v11_drift.sql +24 -0
  119. package/dist/native.d.ts +23 -0
  120. package/dist/native.js +1 -0
  121. package/dist/okf-export.d.ts +3 -0
  122. package/dist/okf-export.js +134 -0
  123. package/dist/okf-import.d.ts +76 -0
  124. package/dist/okf-import.js +271 -0
  125. package/dist/okf.d.ts +107 -0
  126. package/dist/okf.js +303 -0
  127. package/dist/perf.d.ts +35 -0
  128. package/dist/perf.js +174 -0
  129. package/dist/query-tokenizer.d.ts +13 -0
  130. package/dist/query-tokenizer.js +133 -0
  131. package/dist/redact.d.ts +2 -0
  132. package/dist/redact.js +18 -0
  133. package/dist/replay-types.d.ts +69 -0
  134. package/dist/replay-types.js +156 -0
  135. package/dist/replay.d.ts +44 -0
  136. package/dist/replay.js +215 -0
  137. package/dist/sqlite-adapter.d.ts +12 -0
  138. package/dist/sqlite-adapter.js +78 -0
  139. package/dist/time-ms.d.ts +1 -0
  140. package/dist/time-ms.js +16 -0
  141. package/dist/tui-types.d.ts +59 -0
  142. package/dist/tui-types.js +4 -0
  143. package/dist/uuid.d.ts +1 -0
  144. package/dist/uuid.js +50 -0
  145. package/package.json +28 -0
@@ -0,0 +1,24 @@
1
+ import type { Store } from "./Store.js";
2
+ /**
3
+ * K13-008 (D13-04) — core-owns-migrations. Returns the directory containing
4
+ * the SQL migration files, resolved relative to the compiled location.
5
+ * Works both when running from src via tsx (src/.. → migrations) and from
6
+ * dist (dist/migrations).
7
+ */
8
+ export declare function exportMigrationsDir(): string;
9
+ export interface MigrateResult {
10
+ from: string;
11
+ to: string;
12
+ applied: string[];
13
+ }
14
+ export type PostApplyHook = (store: Store) => void;
15
+ export declare const HOOK_NAMES: readonly ["tool.execute.before", "tool.execute.after", "chat.message", "experimental.chat.system.transform", "experimental.session.compacting", "event", "dispose"];
16
+ export declare class Migrate {
17
+ private store;
18
+ private migrationsDir;
19
+ private readonly postApplyHooks;
20
+ constructor(store: Store, migrationsDir: string, postApplyHooks?: Record<string, PostApplyHook>);
21
+ registerPostApply(version: string, hook: PostApplyHook): void;
22
+ run(): Promise<MigrateResult>;
23
+ private listPending;
24
+ }
@@ -0,0 +1,285 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * K13-008 (D13-04) — core-owns-migrations. Returns the directory containing
6
+ * the SQL migration files, resolved relative to the compiled location.
7
+ * Works both when running from src via tsx (src/.. → migrations) and from
8
+ * dist (dist/migrations).
9
+ */
10
+ export function exportMigrationsDir() {
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ const direct = join(here, "migrations");
13
+ if (existsSync(direct))
14
+ return direct;
15
+ return join(here, "..", "migrations");
16
+ }
17
+ // v0.9.0 (K9-001 / plan §5.3, D9-08) — the six hooks Kevin registers, in
18
+ // hook-object order, plus `dispose` (v1.0.0 K10-013): the seventh
19
+ // instrumented hook. Canonical list: the "010" post-apply hook seeds
20
+ // hook_liveness from it, and HookLiveness (K9-009) wraps exactly these
21
+ // names, so a future hook added here is visible in the table on the next
22
+ // migration run.
23
+ export const HOOK_NAMES = [
24
+ "tool.execute.before",
25
+ "tool.execute.after",
26
+ "chat.message",
27
+ "experimental.chat.system.transform",
28
+ "experimental.session.compacting",
29
+ "event",
30
+ "dispose",
31
+ ];
32
+ // Built-in post-apply hooks, keyed by migration version. Each hook runs inside
33
+ // the same transaction as the migration's DDL, so a hook failure rolls back the
34
+ // whole migration. Hooks run when their version is applied. Migration 008's
35
+ // reconciliation hook is also safe to run on later no-op starts (K7-002).
36
+ const DEFAULT_POST_APPLY_HOOKS = {
37
+ // v0.2.0 Signal Quality: defensive backfill of memories.origin for legacy
38
+ // rows. The column is NOT NULL DEFAULT 'agent', so SQLite already populates
39
+ // pre-existing rows with 'agent' on ALTER TABLE. This hook is a belt-and-
40
+ // braces UPDATE that coerces any NULL/empty stragglers (which would only
41
+ // exist if a partial DB skipped the DEFAULT) back to 'agent'.
42
+ "003": (store) => {
43
+ store
44
+ .prepare("UPDATE memories SET origin = 'agent' WHERE origin IS NULL OR origin = ''")
45
+ .run();
46
+ },
47
+ // v0.3.0 Knowledge + Causality: backfill evidence_count and status for
48
+ // legacy rows. Columns have NOT NULL DEFAULT, so SQLite already populates
49
+ // pre-existing rows. This hook is belt-and-braces in case a partial DB
50
+ // skipped the defaults.
51
+ "004": (store) => {
52
+ store
53
+ .prepare("UPDATE memories SET evidence_count = 0 WHERE evidence_count IS NULL")
54
+ .run();
55
+ store
56
+ .prepare("UPDATE memories SET status = 'active' WHERE status IS NULL OR status = ''")
57
+ .run();
58
+ },
59
+ // v0.4.0 Signal over Noise: backfill recurrence_count for legacy rows.
60
+ // recurrence_count has NOT NULL DEFAULT 0, so SQLite already populates
61
+ // pre-existing rows; fix_args and last_injected_at are nullable and need
62
+ // no coercion. This hook is belt-and-braces in case a partial DB skipped
63
+ // the defaults.
64
+ "005": (store) => {
65
+ store
66
+ .prepare("UPDATE memories SET recurrence_count = 0 WHERE recurrence_count IS NULL")
67
+ .run();
68
+ },
69
+ // v0.5.0 (K5-002 / plan §6, D5-13) — Glass Box: re-derive the four
70
+ // injection counters from the ledger table instead of incrementing them.
71
+ // The rebuild remapped prior `effective` rows to `inconclusive`, so any
72
+ // pre-existing counter values would otherwise carry the v0.4 confound
73
+ // forward. Re-derivation is idempotent by construction and self-healing
74
+ // on earlier drift. No INSERTs: the rows are seeded by the migration SQL;
75
+ // a missing row makes the UPDATE a harmless no-op.
76
+ "006": (store) => {
77
+ store
78
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections) WHERE key = 'injections_total'")
79
+ .run();
80
+ store
81
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'effective') WHERE key = 'injections_effective'")
82
+ .run();
83
+ store
84
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'ineffective') WHERE key = 'injections_ineffective'")
85
+ .run();
86
+ store
87
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'inconclusive') WHERE key = 'injections_inconclusive'")
88
+ .run();
89
+ },
90
+ // v0.6.0 (K6-002 / plan §6, D6-08) — Pull: three re-derivations, all
91
+ // idempotent by re-derivation (same discipline as "006", D5-13).
92
+ // 1. Back-fill `inferable = 0` for the four non-`error` types still NULL:
93
+ // they are `non_inferable` by rules 1 and 2 of plan §5.3
94
+ // unconditionally. Guarded by `inferable IS NULL` so a re-run cannot
95
+ // overwrite a classification produced later by inferability.classify().
96
+ // `error` rows are left NULL and classified lazily.
97
+ // 2-3. proposals_created / artifact_writes_total are re-derived from
98
+ // their tables; a missing row makes the UPDATE a harmless no-op.
99
+ "007": (store) => {
100
+ store
101
+ .prepare("UPDATE memories SET inferable = 0 WHERE inferable IS NULL AND type IN ('decision','rule','solution','pattern')")
102
+ .run();
103
+ store
104
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM curation_proposals) WHERE key = 'proposals_created'")
105
+ .run();
106
+ store
107
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM artifact_writes WHERE outcome = 'written') WHERE key = 'artifact_writes_total'")
108
+ .run();
109
+ },
110
+ // v0.7.0 (K7-002 / plan §6, D7-02) — Truth: four re-derivations, all
111
+ // idempotent by re-derivation (same discipline as "006"/"007", D5-13).
112
+ // 1. Normalize any NULL truth_penalty to 0.0 (belt-and-braces; the column
113
+ // is NOT NULL DEFAULT 0.0, so SQLite already back-fills on ALTER TABLE).
114
+ // 2-4. repo_facts_scanned / conflicts_detected / memories_contradicted
115
+ // are re-derived from their tables, NOT trusted from the counters, so
116
+ // the metrics survive a database restored from backup or edited by
117
+ // hand. A missing row makes the UPDATE a harmless no-op.
118
+ "008": (store) => {
119
+ store
120
+ .prepare("UPDATE memories SET truth_penalty = 0.0 WHERE truth_penalty IS NULL")
121
+ .run();
122
+ store
123
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM repo_facts) WHERE key = 'repo_facts_scanned'")
124
+ .run();
125
+ store
126
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM memory_conflicts) WHERE key = 'conflicts_detected'")
127
+ .run();
128
+ store
129
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM memories WHERE truth_penalty > 0.0) WHERE key = 'memories_contradicted'")
130
+ .run();
131
+ },
132
+ // v0.8.0 (K8-002 / plan §6.1, D8-03) — Team: three operations, all
133
+ // idempotent by re-derivation (same discipline as "006"/"007"/"008").
134
+ // 1. Back-fill the new scope from the old one: repo_id = project_id
135
+ // keeps every pre-v0.8 row retrievable on the same machine with an
136
+ // identical result set. Guarded by `repo_id IS NULL`; a row whose
137
+ // project_id is also NULL keeps a NULL repo_id — the retrieval
138
+ // path handles NULL as global (K8-007) instead of the hook faking
139
+ // a value. No git-derived identity here: the hook runs inside a
140
+ // migration and must not read the filesystem (K8-002 criterion).
141
+ // 2. Normalize the layer marker for any row written by a concurrent
142
+ // v0.7.0 process between ALTER and hook (belt and braces; the
143
+ // column DEFAULT already covers the ordinary case).
144
+ // 3. Re-derive shared_entries_total from state rather than trusting
145
+ // an incremented value that may predate a crash.
146
+ "009": (store) => {
147
+ store
148
+ .prepare("UPDATE memories SET repo_id = project_id WHERE repo_id IS NULL")
149
+ .run();
150
+ store
151
+ .prepare("UPDATE memories SET layer = 'local' WHERE layer IS NULL OR layer = ''")
152
+ .run();
153
+ store
154
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM shared_entries) WHERE key = 'shared_entries_total'")
155
+ .run();
156
+ },
157
+ // v0.9.0 (K9-001 / plan §6.1, D9-08) — Native: three operations, all
158
+ // idempotent by design.
159
+ // 1. Seed one hook_liveness row per name in HOOK_NAMES, with the
160
+ // experimental flag derived from the hook's own `experimental.`
161
+ // prefix and every counter at zero. Seeding eagerly (INSERT OR
162
+ // IGNORE) makes a hook that has never fired a visible row with
163
+ // fire_count = 0, not an absent row indistinguishable from a hook
164
+ // Kevin does not register.
165
+ // 2. Re-derive hooks_dead_total from hook_liveness state rather than
166
+ // trusting an incremented value (same discipline as "006"-"009").
167
+ // 3. Normalize any experimental flag that disagrees with its own
168
+ // hook column's prefix — cheap, and it repairs a row hand-edited
169
+ // during debugging.
170
+ "010": (store) => {
171
+ const seed = store.prepare("INSERT OR IGNORE INTO hook_liveness (hook, experimental) VALUES (?, ?)");
172
+ for (const name of HOOK_NAMES) {
173
+ seed.run(name, name.startsWith("experimental.") ? 1 : 0);
174
+ }
175
+ store
176
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM hook_liveness WHERE dead_since IS NOT NULL) WHERE key = 'hooks_dead_total'")
177
+ .run();
178
+ store
179
+ .prepare("UPDATE hook_liveness SET experimental = CASE WHEN hook LIKE 'experimental.%' THEN 1 ELSE 0 END")
180
+ .run();
181
+ },
182
+ // v1.0.0 (K10-005 / plan §6.1) — Proven: four operations, all idempotent.
183
+ // 1. Seed dispose row defensively.
184
+ // 2-3. Re-derive perf_budget_breaches and bench_runs_total.
185
+ // 4. Normalise NULL within_budget to 1.
186
+ "011": (store) => {
187
+ store
188
+ .prepare("INSERT OR IGNORE INTO hook_liveness (hook) VALUES ('dispose')")
189
+ .run();
190
+ store
191
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM perf_samples WHERE within_budget = 0) WHERE key = 'perf_budget_breaches'")
192
+ .run();
193
+ store
194
+ .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM bench_runs) WHERE key = 'bench_runs_total'")
195
+ .run();
196
+ store
197
+ .prepare("UPDATE perf_samples SET within_budget = 1 WHERE within_budget IS NULL")
198
+ .run();
199
+ },
200
+ };
201
+ export class Migrate {
202
+ store;
203
+ migrationsDir;
204
+ postApplyHooks;
205
+ constructor(store, migrationsDir, postApplyHooks) {
206
+ this.store = store;
207
+ this.migrationsDir = migrationsDir;
208
+ this.postApplyHooks = new Map(Object.entries({
209
+ ...DEFAULT_POST_APPLY_HOOKS,
210
+ ...(postApplyHooks ?? {}),
211
+ }));
212
+ }
213
+ registerPostApply(version, hook) {
214
+ this.postApplyHooks.set(version, hook);
215
+ }
216
+ async run() {
217
+ this.store.exec(`CREATE TABLE IF NOT EXISTS schema_version (
218
+ version TEXT PRIMARY KEY,
219
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
220
+ );`);
221
+ const currentRow = this.store
222
+ .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
223
+ .get();
224
+ const from = currentRow?.version ?? "000";
225
+ const pending = this.listPending(from);
226
+ if (pending.length === 0) {
227
+ // v0.7.0 (K7-002) — heal drift in the 008 counters on a no-op
228
+ // startup while preserving `applied: []` idempotency. v0.8.0
229
+ // (K8-002 / plan §6.1) extends the same repair to the 009
230
+ // back-fill and the shared_entries_total re-derivation, and
231
+ // v0.9.0 (K9-001 / plan §6.1) to the 010 seeding,
232
+ // hooks_dead_total re-derivation and experimental
233
+ // normalization: all three hooks are idempotent by guarded
234
+ // updates and re-derivation, so a no-op startup can heal a
235
+ // crash that landed between the DDL and the hook without
236
+ // re-applying DDL.
237
+ if (from === "008" ||
238
+ from === "009" ||
239
+ from === "010" ||
240
+ from === "011") {
241
+ const repairHook = this.postApplyHooks.get(from);
242
+ if (repairHook)
243
+ this.store.transaction(() => repairHook(this.store));
244
+ }
245
+ return { from, to: from, applied: [] };
246
+ }
247
+ const insertVersion = this.store.prepare("INSERT OR IGNORE INTO schema_version (version) VALUES (?)");
248
+ for (const migration of pending) {
249
+ const sql = readFileSync(join(this.migrationsDir, migration.file), "utf8");
250
+ this.store.transaction(() => {
251
+ this.store.exec(sql);
252
+ const hook = this.postApplyHooks.get(migration.version);
253
+ if (hook)
254
+ hook(this.store);
255
+ insertVersion.run(migration.version);
256
+ });
257
+ }
258
+ return {
259
+ from,
260
+ to: pending[pending.length - 1].version,
261
+ applied: pending.map((m) => m.version),
262
+ };
263
+ }
264
+ // v1.1.0 (K11-015) — lexicographic ordering is valid through "999" because
265
+ // versions are zero-padded 3-digit strings ("001" … "999"). Any future
266
+ // migration beyond 999 must use a 4-digit prefix and this comparison must
267
+ // become numeric (parseInt). Until then, string > works and keeps the
268
+ // migration idempotency simple (plan §5.5, D11-??).
269
+ listPending(current) {
270
+ let files = [];
271
+ try {
272
+ files = readdirSync(this.migrationsDir).filter((f) => f.endsWith(".sql"));
273
+ }
274
+ catch {
275
+ return [];
276
+ }
277
+ files.sort();
278
+ return files
279
+ .map((file) => {
280
+ const match = file.match(/^(\w+?)_/);
281
+ return match ? { version: match[1], file } : null;
282
+ })
283
+ .filter((m) => m !== null && m.version > current);
284
+ }
285
+ }
@@ -0,0 +1,49 @@
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
+ }
@@ -0,0 +1,132 @@
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
+ }
@@ -0,0 +1,124 @@
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
+ };