@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,97 @@
1
+ import type { Store } from "./Store.js";
2
+ /**
3
+ * Seeded metric keys defined by migration 003_v02_signal.sql.
4
+ * The order here MUST match the migration's INSERT OR IGNORE block, since
5
+ * snapshot() relies on those keys being present in the cache even when the
6
+ * underlying table is empty (e.g., before 003 is applied, on a fresh
7
+ * :memory: test DB, or after a manual wipe).
8
+ */
9
+ export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked"];
10
+ export type MetricKey = (typeof METRIC_KEYS)[number];
11
+ /**
12
+ * Cheap token estimate used when bumping the `tokens_injected_*` counters.
13
+ * Per plan §B6.2: heuristic = block.length / 4, floored to 1 so empty strings
14
+ * don't contribute zero tokens (avoids losing signal on whitespace-only
15
+ * blocks).
16
+ */
17
+ export declare function estimateTokens(text: string): number;
18
+ /**
19
+ * In-memory mirror of the `kevin_metrics` table with debounced writes.
20
+ *
21
+ * The cache is seeded from `kevin_metrics` on construction (or zeros if the
22
+ * table is missing — graceful degradation for unit tests and pre-003 DBs).
23
+ * `incr()` updates the cache and schedules a debounced `flush()` (1 s by
24
+ * default). `flush()` writes every dirty key in a single transaction and
25
+ * clears the timer, so the call site can also force a flush on `session.idle`
26
+ * and on plugin dispose.
27
+ */
28
+ export declare class Metrics {
29
+ private readonly store;
30
+ private readonly cache;
31
+ private readonly dirty;
32
+ private flushTimer;
33
+ private readonly flushMs;
34
+ private closed;
35
+ constructor(store: Store, flushMs?: number);
36
+ private loadFromDb;
37
+ incr(key: MetricKey, by?: number): void;
38
+ /**
39
+ * v0.6.0 (K6-018/019 / plan §5.8) — the pull-channel registration
40
+ * counters. These live OUTSIDE `METRIC_KEYS`, which is frozen at 33
41
+ * (K7-004 acceptance, the verified cumulative ladder), but persist to
42
+ * the same `kevin_metrics` table so `kevin_audit`'s channels block can
43
+ * read them by SQL (K6-023). They are written immediately (no debounce):
44
+ * they change at most twice per process, on session start.
45
+ */
46
+ incrRegistered(key: "skills_registered" | "references_registered", by?: number): void;
47
+ /**
48
+ * Returns a snapshot of the cache. The returned object always contains all
49
+ * METRIC_KEYS, even if the DB has no rows yet. Does NOT flush.
50
+ */
51
+ snapshot(): Record<MetricKey, number>;
52
+ /**
53
+ * Returns the cached value for a single key. Does NOT flush.
54
+ */
55
+ get(key: MetricKey): number;
56
+ /**
57
+ * v0.4.0 (K4-008): injection precision = effective / total settled
58
+ * injections. 0 when the ledger has no entries yet (no division by zero).
59
+ * Computed from the cached counters — does NOT flush.
60
+ * v0.5.0 (K5-004 / plan §5.1, D5-02) — the denominator is now
61
+ * `effective + ineffective` only. `inconclusive` (the new majority
62
+ * bucket) must not inflate precision: absence of recurrence is not
63
+ * evidence of effect. An idle session therefore no longer drives
64
+ * precision toward 1.0 — expect the reported rate to fall sharply on
65
+ * real databases; that is the intended result.
66
+ */
67
+ precisionRate(): number;
68
+ /**
69
+ * v0.5.0 (K5-004 / plan §5.1, D5-02) — the share of injections that
70
+ * were actually measured (effective + ineffective) of all injections.
71
+ * Reported alongside `precisionRate` so a low measurable fraction is
72
+ * visible rather than hidden behind a large `total`. 0 when the ledger
73
+ * is empty. Computed from the cached counters — does NOT flush.
74
+ */
75
+ coverageRate(): number;
76
+ /**
77
+ * v0.5.0 (K5-004 / plan §5.2, D5-02) — the five `injections_blocked_*`
78
+ * counters keyed by their short names. Consumed by `kevin_audit`.
79
+ * v0.6.0 (K6-004 / plan §8.3) — sixth member keyed `confidence`
80
+ * (the low_confidence gate branch, K6-022).
81
+ * Computed from the cached counters — does NOT flush.
82
+ */
83
+ blockedSnapshot(): Record<string, number>;
84
+ /** True iff a debounced flush is scheduled. Useful for tests. */
85
+ isFlushScheduled(): boolean;
86
+ /**
87
+ * Writes every dirty key to `kevin_metrics` in one transaction. Clears
88
+ * the debounce timer. Safe to call repeatedly; no-op when nothing is
89
+ * dirty or when the object is closed. Missing rows are inserted, present
90
+ * rows are updated. The table is created lazily on first flush so this
91
+ * works against pre-003 DBs too.
92
+ */
93
+ flush(): void;
94
+ close(): void;
95
+ private scheduleFlush;
96
+ private clearTimer;
97
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Seeded metric keys defined by migration 003_v02_signal.sql.
3
+ * The order here MUST match the migration's INSERT OR IGNORE block, since
4
+ * snapshot() relies on those keys being present in the cache even when the
5
+ * underlying table is empty (e.g., before 003 is applied, on a fresh
6
+ * :memory: test DB, or after a manual wipe).
7
+ */
8
+ export const METRIC_KEYS = [
9
+ "tokens_injected_pre_prompt",
10
+ "tokens_injected_compacting",
11
+ "reflections_throttled",
12
+ "duplicate_suppressions",
13
+ "tool_calls_deduped",
14
+ "patterns_mined",
15
+ "patterns_causal",
16
+ "causal_links",
17
+ "memories_superseded",
18
+ "injections_total",
19
+ "injections_effective",
20
+ "injections_ineffective",
21
+ "patterns_promoted_new",
22
+ // v0.5.0 (K5-004 / plan §8.3) — order matches migration 006's seed block.
23
+ "injections_inconclusive",
24
+ "injections_blocked_seen",
25
+ "injections_blocked_weak",
26
+ "injections_blocked_recurrence",
27
+ "injections_blocked_stale",
28
+ "injections_blocked_ignored",
29
+ "feedback_positive_total",
30
+ "feedback_negative_total",
31
+ "memories_archived",
32
+ // v0.6.0 (K6-004 / plan §8.3) — order matches migration 007's seed block.
33
+ // injections_blocked_confidence is the sixth member of the blocked
34
+ // family and MUST be counted like the other five (Principle 16).
35
+ "proposals_created",
36
+ "proposals_approved",
37
+ "proposals_rejected",
38
+ "artifact_writes_total",
39
+ "artifact_writes_noop",
40
+ "injections_blocked_confidence",
41
+ // v0.7.0 (K7-004 / plan §8.3) — order matches migration 008's seed block.
42
+ // These are the Project Truth counters. No per-type precision helper is
43
+ // exported here: the per-type split of plan §5.6 lives in `kevin_audit`
44
+ // as pure SQL, not as a `Metrics` method, so there is never a second
45
+ // definition of precision to drift from the first (D7-14).
46
+ "repo_facts_scanned",
47
+ "memories_contradicted",
48
+ "conventions_mined",
49
+ "conflicts_detected",
50
+ "error_lessons_suppressed",
51
+ // v0.8.0 (K8-003 / plan §8.3) — order matches migration 009's seed block.
52
+ // These are the Team counters. shared_entries_total is re-derived from
53
+ // the table by the 009 post-apply hook, never trusted from increments.
54
+ "shared_entries_total",
55
+ "shared_entries_imported",
56
+ "shared_entries_exported",
57
+ "okf_merge_folds",
58
+ "rekey_events",
59
+ "injections_from_shared",
60
+ // v1.1.0 (K11-001 / plan §4, D11-01) — drift metrics; order matches 012 seed.
61
+ "bench_regression_failures",
62
+ "forget_requests_total",
63
+ "forget_tombstones_published",
64
+ // v1.2.0 (K12-001 / plan §4) — surface metrics; no migration this
65
+ // release — rows are created on first incr via upsert (K12-001).
66
+ "tui_snapshots_flushed",
67
+ "tui_actions_invoked",
68
+ ];
69
+ const DEFAULT_FLUSH_MS = 1000;
70
+ function zeroCache() {
71
+ const m = new Map();
72
+ for (const k of METRIC_KEYS)
73
+ m.set(k, 0);
74
+ return m;
75
+ }
76
+ /**
77
+ * Cheap token estimate used when bumping the `tokens_injected_*` counters.
78
+ * Per plan §B6.2: heuristic = block.length / 4, floored to 1 so empty strings
79
+ * don't contribute zero tokens (avoids losing signal on whitespace-only
80
+ * blocks).
81
+ */
82
+ export function estimateTokens(text) {
83
+ return Math.max(1, Math.round(text.length / 4));
84
+ }
85
+ /**
86
+ * In-memory mirror of the `kevin_metrics` table with debounced writes.
87
+ *
88
+ * The cache is seeded from `kevin_metrics` on construction (or zeros if the
89
+ * table is missing — graceful degradation for unit tests and pre-003 DBs).
90
+ * `incr()` updates the cache and schedules a debounced `flush()` (1 s by
91
+ * default). `flush()` writes every dirty key in a single transaction and
92
+ * clears the timer, so the call site can also force a flush on `session.idle`
93
+ * and on plugin dispose.
94
+ */
95
+ export class Metrics {
96
+ store;
97
+ cache;
98
+ dirty = new Set();
99
+ flushTimer = null;
100
+ flushMs;
101
+ closed = false;
102
+ constructor(store, flushMs = DEFAULT_FLUSH_MS) {
103
+ this.store = store;
104
+ this.flushMs = flushMs;
105
+ this.cache = zeroCache();
106
+ this.loadFromDb();
107
+ }
108
+ loadFromDb() {
109
+ // Graceful: kevin_metrics only exists after migration 003. If a caller
110
+ // instantiates Metrics against a fresh / pre-003 DB, leave the zeros
111
+ // seeded in memory; the eventual flush() will create the rows.
112
+ let rows = [];
113
+ try {
114
+ rows = this.store
115
+ .prepare("SELECT key, value FROM kevin_metrics")
116
+ .all();
117
+ }
118
+ catch {
119
+ rows = [];
120
+ }
121
+ for (const row of rows) {
122
+ if (this.cache.has(row.key)) {
123
+ this.cache.set(row.key, row.value);
124
+ }
125
+ }
126
+ }
127
+ incr(key, by = 1) {
128
+ if (this.closed)
129
+ return;
130
+ const current = this.cache.get(key) ?? 0;
131
+ this.cache.set(key, current + by);
132
+ this.dirty.add(key);
133
+ this.scheduleFlush();
134
+ }
135
+ /**
136
+ * v0.6.0 (K6-018/019 / plan §5.8) — the pull-channel registration
137
+ * counters. These live OUTSIDE `METRIC_KEYS`, which is frozen at 33
138
+ * (K7-004 acceptance, the verified cumulative ladder), but persist to
139
+ * the same `kevin_metrics` table so `kevin_audit`'s channels block can
140
+ * read them by SQL (K6-023). They are written immediately (no debounce):
141
+ * they change at most twice per process, on session start.
142
+ */
143
+ incrRegistered(key, by = 1) {
144
+ if (this.closed)
145
+ return;
146
+ const store = this.store;
147
+ store.transaction(() => {
148
+ store.exec(`CREATE TABLE IF NOT EXISTS kevin_metrics (
149
+ key TEXT PRIMARY KEY,
150
+ value INTEGER NOT NULL DEFAULT 0,
151
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
152
+ )`);
153
+ store
154
+ .prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
155
+ VALUES (?, ?, datetime('now'))
156
+ ON CONFLICT(key) DO UPDATE SET
157
+ value = value + excluded.value,
158
+ updated_at = datetime('now')`)
159
+ .run(key, by);
160
+ });
161
+ }
162
+ /**
163
+ * Returns a snapshot of the cache. The returned object always contains all
164
+ * METRIC_KEYS, even if the DB has no rows yet. Does NOT flush.
165
+ */
166
+ snapshot() {
167
+ const out = {};
168
+ for (const k of METRIC_KEYS)
169
+ out[k] = this.cache.get(k) ?? 0;
170
+ return out;
171
+ }
172
+ /**
173
+ * Returns the cached value for a single key. Does NOT flush.
174
+ */
175
+ get(key) {
176
+ return this.cache.get(key) ?? 0;
177
+ }
178
+ /**
179
+ * v0.4.0 (K4-008): injection precision = effective / total settled
180
+ * injections. 0 when the ledger has no entries yet (no division by zero).
181
+ * Computed from the cached counters — does NOT flush.
182
+ * v0.5.0 (K5-004 / plan §5.1, D5-02) — the denominator is now
183
+ * `effective + ineffective` only. `inconclusive` (the new majority
184
+ * bucket) must not inflate precision: absence of recurrence is not
185
+ * evidence of effect. An idle session therefore no longer drives
186
+ * precision toward 1.0 — expect the reported rate to fall sharply on
187
+ * real databases; that is the intended result.
188
+ */
189
+ precisionRate() {
190
+ const effective = this.cache.get("injections_effective") ?? 0;
191
+ const measured = effective + (this.cache.get("injections_ineffective") ?? 0);
192
+ if (measured <= 0)
193
+ return 0;
194
+ return Math.min(1, effective / measured);
195
+ }
196
+ /**
197
+ * v0.5.0 (K5-004 / plan §5.1, D5-02) — the share of injections that
198
+ * were actually measured (effective + ineffective) of all injections.
199
+ * Reported alongside `precisionRate` so a low measurable fraction is
200
+ * visible rather than hidden behind a large `total`. 0 when the ledger
201
+ * is empty. Computed from the cached counters — does NOT flush.
202
+ */
203
+ coverageRate() {
204
+ const total = this.cache.get("injections_total") ?? 0;
205
+ if (total <= 0)
206
+ return 0;
207
+ const measured = (this.cache.get("injections_effective") ?? 0) +
208
+ (this.cache.get("injections_ineffective") ?? 0);
209
+ return Math.min(1, measured / total);
210
+ }
211
+ /**
212
+ * v0.5.0 (K5-004 / plan §5.2, D5-02) — the five `injections_blocked_*`
213
+ * counters keyed by their short names. Consumed by `kevin_audit`.
214
+ * v0.6.0 (K6-004 / plan §8.3) — sixth member keyed `confidence`
215
+ * (the low_confidence gate branch, K6-022).
216
+ * Computed from the cached counters — does NOT flush.
217
+ */
218
+ blockedSnapshot() {
219
+ return {
220
+ seen: this.cache.get("injections_blocked_seen") ?? 0,
221
+ weak: this.cache.get("injections_blocked_weak") ?? 0,
222
+ recurrence: this.cache.get("injections_blocked_recurrence") ?? 0,
223
+ stale: this.cache.get("injections_blocked_stale") ?? 0,
224
+ ignored: this.cache.get("injections_blocked_ignored") ?? 0,
225
+ confidence: this.cache.get("injections_blocked_confidence") ?? 0,
226
+ };
227
+ }
228
+ /** True iff a debounced flush is scheduled. Useful for tests. */
229
+ isFlushScheduled() {
230
+ return this.flushTimer !== null;
231
+ }
232
+ /**
233
+ * Writes every dirty key to `kevin_metrics` in one transaction. Clears
234
+ * the debounce timer. Safe to call repeatedly; no-op when nothing is
235
+ * dirty or when the object is closed. Missing rows are inserted, present
236
+ * rows are updated. The table is created lazily on first flush so this
237
+ * works against pre-003 DBs too.
238
+ */
239
+ flush() {
240
+ if (this.closed || this.dirty.size === 0) {
241
+ this.clearTimer();
242
+ return;
243
+ }
244
+ this.clearTimer();
245
+ const dirtyKeys = Array.from(this.dirty);
246
+ this.dirty.clear();
247
+ const store = this.store;
248
+ store.transaction(() => {
249
+ store.exec(`CREATE TABLE IF NOT EXISTS kevin_metrics (
250
+ key TEXT PRIMARY KEY,
251
+ value INTEGER NOT NULL DEFAULT 0,
252
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
253
+ )`);
254
+ const upsert = store.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
255
+ VALUES (?, ?, datetime('now'))
256
+ ON CONFLICT(key) DO UPDATE SET
257
+ value = excluded.value,
258
+ updated_at = datetime('now')`);
259
+ for (const k of dirtyKeys) {
260
+ upsert.run(k, this.cache.get(k) ?? 0);
261
+ }
262
+ });
263
+ }
264
+ close() {
265
+ if (this.closed)
266
+ return;
267
+ this.flush();
268
+ this.closed = true;
269
+ this.clearTimer();
270
+ }
271
+ scheduleFlush() {
272
+ if (this.flushTimer !== null)
273
+ return;
274
+ this.flushTimer = setTimeout(() => {
275
+ this.flushTimer = null;
276
+ this.flush();
277
+ }, this.flushMs);
278
+ // unref so the timer never keeps a Node process alive on its own.
279
+ const t = this.flushTimer;
280
+ t.unref?.();
281
+ }
282
+ clearTimer() {
283
+ if (this.flushTimer !== null) {
284
+ clearTimeout(this.flushTimer);
285
+ this.flushTimer = null;
286
+ }
287
+ }
288
+ }
@@ -0,0 +1,92 @@
1
+ -- ============================================================
2
+ -- Kevin 0.1.0 — Schema inicial
3
+ -- ============================================================
4
+
5
+ -- Tabla de versiones para migraciones
6
+ CREATE TABLE IF NOT EXISTS schema_version (
7
+ version TEXT PRIMARY KEY,
8
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
9
+ );
10
+
11
+ -- ============================================================
12
+ -- memories: lecciones aprendidas
13
+ -- ============================================================
14
+ CREATE TABLE IF NOT EXISTS memories (
15
+ id TEXT PRIMARY KEY,
16
+ type TEXT NOT NULL CHECK(type IN ('error', 'pattern', 'decision', 'context')),
17
+ content TEXT NOT NULL,
18
+ scope TEXT NOT NULL DEFAULT 'project' CHECK(scope IN ('project', 'session')),
19
+ relevance_score REAL DEFAULT 0.5,
20
+ source_tool TEXT,
21
+ source_session TEXT,
22
+ metadata TEXT,
23
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
24
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
25
+ expires_at TEXT
26
+ );
27
+
28
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
29
+ CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
30
+ CREATE INDEX IF NOT EXISTS idx_memories_relevance ON memories(relevance_score DESC);
31
+ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
32
+
33
+ -- FTS5: búsqueda full-text con remoción de diacríticos (mejor para español)
34
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
35
+ content,
36
+ content='memories',
37
+ tokenize='unicode61 remove_diacritics 1'
38
+ );
39
+
40
+ -- Triggers para mantener FTS5 sincronizado (FTS5 external-content se indexa por rowid)
41
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
42
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
43
+ END;
44
+
45
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
46
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
47
+ END;
48
+
49
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
50
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
51
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
52
+ END;
53
+
54
+ -- ============================================================
55
+ -- tool_calls: observación de tool calls del agente
56
+ -- ============================================================
57
+ CREATE TABLE IF NOT EXISTS tool_calls (
58
+ id TEXT PRIMARY KEY,
59
+ session_id TEXT NOT NULL,
60
+ ts TEXT NOT NULL DEFAULT (datetime('now')),
61
+ tool TEXT NOT NULL,
62
+ args_summary TEXT,
63
+ success INTEGER NOT NULL CHECK(success IN (0,1)),
64
+ duration_ms INTEGER,
65
+ agent TEXT,
66
+ error_type TEXT,
67
+ metadata TEXT
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
71
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool);
72
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_ts ON tool_calls(ts);
73
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_success ON tool_calls(success);
74
+
75
+ -- ============================================================
76
+ -- retrospectives: resúmenes de sesión
77
+ -- ============================================================
78
+ CREATE TABLE IF NOT EXISTS retrospectives (
79
+ id TEXT PRIMARY KEY,
80
+ session_id TEXT NOT NULL,
81
+ ts TEXT NOT NULL DEFAULT (datetime('now')),
82
+ failure_count INTEGER DEFAULT 0,
83
+ success_count INTEGER DEFAULT 0,
84
+ lessons_count INTEGER DEFAULT 0,
85
+ file_path TEXT,
86
+ metadata TEXT
87
+ );
88
+
89
+ -- ============================================================
90
+ -- Seed: versión inicial
91
+ -- ============================================================
92
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('001');
@@ -0,0 +1,14 @@
1
+ -- ============================================================
2
+ -- Kevin 0.1.1 — Migration 002: indexes adicionales
3
+ -- ============================================================
4
+
5
+ -- F#29: uniqueness on retrospectives.session_id
6
+ -- Prevents duplicate retrospective rows under concurrent session.idle events.
7
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_retrospectives_session
8
+ ON retrospectives(session_id);
9
+
10
+ -- F#31: index on memories.expires_at
11
+ -- Every query/queryRelevant/loadAll filters WHERE (expires_at IS NULL OR expires_at > datetime('now')).
12
+ -- Without this index the filter is a linear scan on large tables.
13
+ CREATE INDEX IF NOT EXISTS idx_memories_expires
14
+ ON memories(expires_at);
@@ -0,0 +1,58 @@
1
+ -- ============================================================
2
+ -- Kevin 0.2.0 — Migration 003: Signal Quality (additive)
3
+ -- ============================================================
4
+ -- Backward-compatible, additive only. All new columns are
5
+ -- nullable or carry a NOT NULL DEFAULT so legacy rows keep
6
+ -- working without a destructive rebuild. The partial UNIQUE
7
+ -- index excludes NULL fingerprints so legacy error memories
8
+ -- do not collide. The schema_version table (created by 001)
9
+ -- guarantees this file runs at most once per DB.
10
+ -- ============================================================
11
+
12
+ -- 1. memories: Signal Quality columns.
13
+ -- project_id — first-class scoping dimension (D2-11).
14
+ -- fingerprint — stable hash of normalized error content, salted by project_id (D2-14).
15
+ -- origin — distinguishes reflector-sourced vs agent-sourced memories (D2-06),
16
+ -- with a CHECK enum and DEFAULT 'agent' so legacy rows backfill cleanly.
17
+ ALTER TABLE memories ADD COLUMN project_id TEXT;
18
+ ALTER TABLE memories ADD COLUMN fingerprint TEXT;
19
+ ALTER TABLE memories ADD COLUMN origin TEXT NOT NULL DEFAULT 'agent'
20
+ CHECK(origin IN ('reflector', 'agent', 'pattern', 'retrospective'));
21
+
22
+ -- 2. tool_calls: same scoping/fingerprint columns for dedup and PatternMiner.
23
+ ALTER TABLE tool_calls ADD COLUMN project_id TEXT;
24
+ ALTER TABLE tool_calls ADD COLUMN fingerprint TEXT;
25
+
26
+ -- 3. Partial UNIQUE: one reflector-sourced error memory per (project_id, fingerprint).
27
+ -- NULL fingerprints are excluded (no dedup for non-error memories or legacy rows).
28
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_memories_error_fp
29
+ ON memories (project_id, fingerprint)
30
+ WHERE type = 'error' AND fingerprint IS NOT NULL AND origin = 'reflector';
31
+
32
+ -- 4. kevin_metrics: seeded counters surfaced by kevin_status (D2-07 / K2-004).
33
+ CREATE TABLE IF NOT EXISTS kevin_metrics (
34
+ key TEXT PRIMARY KEY,
35
+ value INTEGER NOT NULL DEFAULT 0,
36
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
37
+ );
38
+
39
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
40
+ ('tokens_injected_pre_prompt', 0),
41
+ ('tokens_injected_compacting', 0),
42
+ ('reflections_throttled', 0),
43
+ ('duplicate_suppressions', 0),
44
+ ('tool_calls_deduped', 0),
45
+ ('patterns_mined', 0);
46
+
47
+ -- 5. kevin_settings: opt-in feature flags (PatternMiner off, tool_calls dedup off).
48
+ CREATE TABLE IF NOT EXISTS kevin_settings (
49
+ key TEXT PRIMARY KEY,
50
+ value TEXT NOT NULL
51
+ );
52
+
53
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
54
+ ('patternminer_enabled', '0'),
55
+ ('tool_calls_dedup_enabled', '0');
56
+
57
+ -- 6. Seed version 003.
58
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('003');