@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,462 +0,0 @@
1
- // ============================================================
2
- // Kevin 0.8.0 — SharedLayer (F3, K8-016 … K8-020)
3
- // ============================================================
4
- // The two-directional bridge between the OKF file and the local
5
- // store. Import may retire a memory — but only on an explicit
6
- // committed tombstone. Export may write a file — but only through
7
- // the one write path that has existed since v0.6.0 (D8-08).
8
- //
9
- // Every statement filters on `repo_id` (D8-02): there is no
10
- // unscoped read or write of `shared_entries` or `okf_imports`.
11
- // ============================================================
12
- import { readFileSync } from "node:fs";
13
- import { computeConfidence } from "./confidence.js";
14
- import { fnv1a64 } from "./fingerprint.js";
15
- import { MAX_ENTRIES, MAX_LINE_BYTES, OKF_VERSION, canonicalize, computeEntryId, deriveConfidence, merge, parse, serialize, } from "./okf.js";
16
- import { uuidv7 } from "./uuid.js";
17
- /**
18
- * Project a local memory into its OKF candidate (K8-020): the
19
- * identifying triple (type, statement, scope) determines the
20
- * entry_id, so a re-export of the same memory is byte-identical.
21
- * created_at is normalized from SQLite's "YYYY-MM-DD HH:MM:SS" to
22
- * the file's ISO-8601 form, keeping join()'s min from flipping
23
- * formats across exports.
24
- */
25
- function memoryToEntry(row) {
26
- return {
27
- entry_id: computeEntryId(row.type, row.content, row.scope),
28
- type: row.type,
29
- statement: row.content,
30
- scope: row.scope,
31
- evidence: row.evidence_count ?? 0,
32
- recurrence: row.recurrence_count ?? 0,
33
- origin: row.origin ?? "pattern",
34
- author_hash: null,
35
- op: "assert",
36
- created_at: `${(row.created_at ?? "").replace(" ", "T")}Z`,
37
- supersedes: null,
38
- };
39
- }
40
- export class SharedLayer {
41
- store;
42
- repoId;
43
- projectId;
44
- version;
45
- writer;
46
- constructor(deps) {
47
- this.store = deps.store;
48
- this.repoId = deps.repoId;
49
- this.projectId = deps.projectId;
50
- this.version = deps.version;
51
- this.writer = deps.writer;
52
- }
53
- /**
54
- * The single whole-file (mode `whole`) construction site in
55
- * SharedLayer (K8-019 / K8-020, D8-08, asserted by
56
- * tests/unit/single_write_path.test.ts): the OKF file is written
57
- * through the ArtifactWriter's write funnel like every other file
58
- * Kevin owns — there is no second write path.
59
- */
60
- wholeRequest(path, content, refusal) {
61
- return {
62
- path,
63
- mode: "whole",
64
- content,
65
- ...(refusal !== undefined ? { refusal } : {}),
66
- };
67
- }
68
- /**
69
- * Pure export planner (K8-020 / plan §5.5): reads the current file
70
- * and merges the given local memories' projections into the corpus,
71
- * producing a {@link WritePlan} — but writes nothing. Refusals are
72
- * audited like any other write: `after === before`, both hashes
73
- * recorded (D6-04). The refusal ladder, in the order the code
74
- * evaluates it — file-side refusals first (not_okf, version_ahead,
75
- * repo_mismatch), then candidate-side refusals in the loop order
76
- * (line_too_long, below_floor, not_curated, unknown_entry), then
77
- * too_many_entries and finally parse_damaged.
78
- */
79
- planExport(memoryIds, path) {
80
- const { exists, parsed } = this.readWithFlag(path);
81
- if (exists && parsed.version === 0) {
82
- return this.refused(path, "not_okf");
83
- }
84
- if (exists && parsed.version > OKF_VERSION) {
85
- return this.refused(path, "version_ahead");
86
- }
87
- if (parsed.repoId !== null && parsed.repoId !== this.repoId) {
88
- return this.refused(path, "repo_mismatch");
89
- }
90
- const candidates = this.candidateEntries(memoryIds);
91
- if (candidates.refusal !== null) {
92
- return this.refused(path, candidates.refusal);
93
- }
94
- const merged = merge(parsed.entries, candidates.entries);
95
- if (merged.length > MAX_ENTRIES) {
96
- return this.refused(path, "too_many_entries");
97
- }
98
- if (exists && parsed.rejected.length > 0) {
99
- return this.refused(path, "parse_damaged");
100
- }
101
- const content = serialize(merged, this.repoId, this.version);
102
- const existing = new Set(parsed.entries.map((e) => e.entry_id));
103
- const entriesAdded = merged.filter((e) => !existing.has(e.entry_id)).length;
104
- const request = this.wholeRequest(path, content);
105
- return { path, request, write: this.writer.plan(request), entriesAdded };
106
- }
107
- /**
108
- * Tombstone-append planner (K8-020 / D8-09): emits one `tombstone`
109
- * line per entry id, never deletes a line. The identifying triple is
110
- * reconstructed from the local projection (a memory with
111
- * `shared_entry_id`) so the line still passes parse's
112
- * tamper-evident entry_id check; an id with no local projection is
113
- * refused rather than emitting an invalid line.
114
- */
115
- planTombstone(entryIds, path) {
116
- const { exists, parsed } = this.readWithFlag(path);
117
- if (exists && parsed.version === 0) {
118
- return this.refused(path, "not_okf");
119
- }
120
- if (exists && parsed.version > OKF_VERSION) {
121
- return this.refused(path, "version_ahead");
122
- }
123
- if (parsed.repoId !== null && parsed.repoId !== this.repoId) {
124
- return this.refused(path, "repo_mismatch");
125
- }
126
- if (exists && parsed.rejected.length > 0) {
127
- return this.refused(path, "parse_damaged");
128
- }
129
- if (entryIds.length === 0) {
130
- const content = serialize(parsed.entries, this.repoId, this.version);
131
- const request = this.wholeRequest(path, content);
132
- return {
133
- path,
134
- request,
135
- write: this.writer.plan(request),
136
- entriesAdded: 0,
137
- };
138
- }
139
- const unique = [...new Set(entryIds)];
140
- const placeholders = unique.map(() => "?").join(", ");
141
- const rows = this.store
142
- .prepare(`SELECT type, content, scope, evidence_count, recurrence_count,
143
- origin, created_at
144
- FROM memories
145
- WHERE shared_entry_id IN (${placeholders}) AND repo_id = ?`)
146
- .all(...unique, this.repoId);
147
- if (rows.length !== entryIds.length) {
148
- return this.refused(path, "unknown_entry");
149
- }
150
- const tombstones = rows.map((row) => ({
151
- ...memoryToEntry(row),
152
- op: "tombstone",
153
- }));
154
- const merged = merge(parsed.entries, tombstones);
155
- if (merged.length > MAX_ENTRIES) {
156
- return this.refused(path, "too_many_entries");
157
- }
158
- const content = serialize(merged, this.repoId, this.version);
159
- const existing = new Set(parsed.entries.map((e) => e.entry_id));
160
- const entriesAdded = merged.filter((e) => !existing.has(e.entry_id)).length;
161
- const request = this.wholeRequest(path, content);
162
- return { path, request, write: this.writer.plan(request), entriesAdded };
163
- }
164
- /**
165
- * v0.8.0 (BUG-003) — heal a stale `#repo` header after a rekey.
166
- * Rekey changes the scope the file is written under; a stale first
167
- * line would make every later planExport/planTombstone refuse with
168
- * repo_mismatch forever. Only the header line is replaced; the entry
169
- * bytes, line endings and trailing newline are preserved. The write
170
- * goes through the single funnel (D8-08). Returns true when the
171
- * header was rewritten. Never throws: a missing or unreadable file
172
- * is nothing to heal.
173
- */
174
- healHeader(path, newRepoId) {
175
- let text;
176
- try {
177
- text = readFileSync(path, "utf8");
178
- }
179
- catch {
180
- return false;
181
- }
182
- const eol = text.includes("\r\n") ? "\r\n" : "\n";
183
- const lines = text.split(/\r?\n/);
184
- const header = `#repo ${newRepoId}`;
185
- const repoLine = lines.findIndex((l) => l.startsWith("#repo "));
186
- if (repoLine === -1 || lines[repoLine] === header)
187
- return false;
188
- lines[repoLine] = header;
189
- this.writer.write(this.wholeRequest(path, lines.join(eol)));
190
- return true;
191
- }
192
- /**
193
- * Apply a previously planned export: the write goes through the
194
- * single write funnel (re-planning against the file's current state),
195
- * and the outcome is recorded on the returned plan (K8-020).
196
- */
197
- applyExport(plan) {
198
- return { ...plan, applied: this.writer.write(plan.request) };
199
- }
200
- /** Refusal plans are audited with both hashes (after === before). */
201
- refused(path, reason) {
202
- const request = this.wholeRequest(path, "", reason);
203
- return {
204
- path,
205
- request,
206
- write: this.writer.plan(request),
207
- entriesAdded: 0,
208
- };
209
- }
210
- /**
211
- * Convert the requested local memories into OKF candidates and run
212
- * the candidate-side refusals — line_too_long, below_floor,
213
- * not_curated — in plan §5.5 order.
214
- */
215
- candidateEntries(memoryIds) {
216
- if (memoryIds.length === 0) {
217
- return { entries: [], refusal: null };
218
- }
219
- const placeholders = memoryIds.map(() => "?").join(", ");
220
- const rows = this.store
221
- .prepare(`SELECT type, content, scope, evidence_count, recurrence_count,
222
- origin, created_at, curated
223
- FROM memories WHERE id IN (${placeholders}) AND repo_id = ?`)
224
- .all(...memoryIds, this.repoId);
225
- // v0.8.0 (BUG-006) — every requested id must have a local
226
- // projection, exactly like planTombstone: a typo'd or foreign id
227
- // is refused instead of silently sharing the matching subset.
228
- if (rows.length !== memoryIds.length) {
229
- return { entries: [], refusal: "unknown_entry" };
230
- }
231
- const requiresApproval = this.setting("share_requires_approval", "1") === "1";
232
- const floor = Number.parseFloat(this.setting("shared_confidence_floor", "0.7"));
233
- const clampedFloor = Number.isNaN(floor)
234
- ? 0.7
235
- : Math.min(1, Math.max(0, floor));
236
- const entries = [];
237
- for (const row of rows) {
238
- const entry = memoryToEntry(row);
239
- if (Buffer.byteLength(canonicalize(entry), "utf8") > MAX_LINE_BYTES) {
240
- return { entries: [], refusal: "line_too_long" };
241
- }
242
- if (computeConfidence(entry.evidence, entry.recurrence) < clampedFloor) {
243
- return { entries: [], refusal: "below_floor" };
244
- }
245
- if (requiresApproval && row.curated !== 1) {
246
- return { entries: [], refusal: "not_curated" };
247
- }
248
- entries.push(entry);
249
- }
250
- return { entries, refusal: null };
251
- }
252
- setting(key, fallback) {
253
- const row = this.store
254
- .prepare("SELECT value FROM kevin_settings WHERE key = ?")
255
- .get(key);
256
- return row?.value ?? fallback;
257
- }
258
- readWithFlag(path) {
259
- let text;
260
- try {
261
- text = readFileSync(path, "utf8");
262
- }
263
- catch {
264
- return {
265
- exists: false,
266
- parsed: {
267
- version: 0,
268
- repoId: null,
269
- entries: [],
270
- rejected: [],
271
- folded: 0,
272
- },
273
- };
274
- }
275
- return { exists: true, parsed: parse(text) };
276
- }
277
- /**
278
- * Read and parse the shared file. Total: a missing or unreadable
279
- * file yields an empty ParseResult, never a throw (D8-14).
280
- */
281
- read(path) {
282
- let text;
283
- try {
284
- text = readFileSync(path, "utf8");
285
- }
286
- catch {
287
- return {
288
- version: 0,
289
- repoId: null,
290
- entries: [],
291
- rejected: [],
292
- folded: 0,
293
- };
294
- }
295
- return parse(text);
296
- }
297
- /**
298
- * Idempotent and cheap to call (K8-016 / plan §5.5): hash the file
299
- * first and return `{ skipped: true }` when the hash matches the
300
- * most recent `okf_imports` row for this repo_id — the session.idle
301
- * path costs one readFileSync plus one hash on an unchanged
302
- * repository. A missing file is not an error: it returns
303
- * `fileHash: null`, `parsed: 0`, and still writes an audit row.
304
- */
305
- import(path) {
306
- let fileHash = null;
307
- let text;
308
- try {
309
- text = readFileSync(path, "utf8");
310
- fileHash = fnv1a64(text);
311
- }
312
- catch {
313
- fileHash = null;
314
- this.writeImportRow(path, null, 0, 0, 0, 0);
315
- return {
316
- path,
317
- fileHash: null,
318
- parsed: 0,
319
- folded: 0,
320
- rejected: 0,
321
- imported: 0,
322
- tombstoned: 0,
323
- skipped: false,
324
- };
325
- }
326
- const last = this.store
327
- .prepare(`SELECT file_hash FROM okf_imports
328
- WHERE repo_id = ? ORDER BY imported_at DESC, rowid DESC LIMIT 1`)
329
- .get(this.repoId);
330
- if (last !== undefined && last.file_hash === fileHash) {
331
- this.writeImportRow(path, fileHash, 0, 0, 0, 1);
332
- return {
333
- path,
334
- fileHash,
335
- parsed: 0,
336
- folded: 0,
337
- rejected: 0,
338
- imported: 0,
339
- tombstoned: 0,
340
- skipped: true,
341
- };
342
- }
343
- const result = parse(text);
344
- const imported = this.upsertEntries(result.entries);
345
- const tombstoned = this.projectEntries(result.entries);
346
- if (result.folded > 0) {
347
- // A persistently non-zero value is signal, not error: the team
348
- // is editing the same entries concurrently and the lattice is
349
- // absorbing it (K8-016).
350
- this.store
351
- .prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
352
- VALUES ('okf_merge_folds', ?, datetime('now'))
353
- ON CONFLICT(key) DO UPDATE SET
354
- value = value + excluded.value,
355
- updated_at = datetime('now')`)
356
- .run(result.folded);
357
- }
358
- this.writeImportRow(path, fileHash, result.entries.length, result.folded, result.rejected.length, 0);
359
- return {
360
- path,
361
- fileHash,
362
- parsed: result.entries.length,
363
- folded: result.folded,
364
- rejected: result.rejected.length,
365
- imported,
366
- tombstoned,
367
- skipped: false,
368
- };
369
- }
370
- writeImportRow(path, fileHash, parsed, folded, rejected, skipped) {
371
- this.store
372
- .prepare(`INSERT INTO okf_imports
373
- (id, repo_id, path, file_hash, entries_parsed, entries_folded,
374
- entries_rejected, skipped)
375
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
376
- .run(uuidv7(), this.repoId, path, fileHash, parsed, folded, rejected, skipped);
377
- }
378
- /**
379
- * Project every imported entry into memories (K8-017 / plan §5.5,
380
- * D8-10): a shared memory IS a memory, so getRelevant(), rankScore(),
381
- * the five gates, truth_penalty, ConflictDetector and every audit
382
- * rollup keep working unchanged.
383
- *
384
- * - assert entries: inserted once, never updated — re-importing an
385
- * unchanged file must not create duplicates, bump evidence_count or
386
- * move updated_at. `fingerprint` is deliberately left NULL: it is a
387
- * different identity dimension (plan §3.3) and conflating it with
388
- * `shared_entry_id` would make the v0.4.0 supersede path fire on
389
- * unrelated entries (K8-017 acceptance).
390
- * - tombstone entries: the ONE place in this codebase where the shared
391
- * layer may write memories.status. This is NOT a v0.7.0 Principle 24
392
- * violation: that principle forbids *contradiction* — a fuzzy
393
- * inference — from writing status, and a tombstone is the opposite:
394
- * an explicit, committed, human-reviewed decision that arrived
395
- * through a pull request (D8-09, plan §5.5).
396
- *
397
- * Returns the number of memories archived by tombstones.
398
- */
399
- projectEntries(entries) {
400
- const findMemory = this.store.prepare(`SELECT id FROM memories
401
- WHERE shared_entry_id = ? AND repo_id = ? LIMIT 1`);
402
- const insertMemory = this.store.prepare(`INSERT INTO memories
403
- (id, type, content, scope, relevance_score, project_id, origin,
404
- evidence_count, recurrence_count, created_at, updated_at, status,
405
- curated, inferable, layer, repo_id, shared_entry_id)
406
- VALUES (?, ?, ?, ?, ?, ?, 'imported', ?, ?, datetime('now'),
407
- datetime('now'), 'active', 1, 1, 'shared', ?, ?)`);
408
- const tombstone = this.store.prepare(`UPDATE memories SET status = 'archived'
409
- WHERE shared_entry_id = ? AND repo_id = ? AND layer = 'shared'
410
- AND status = 'active'`);
411
- const lastChanges = this.store.prepare("SELECT changes() AS c");
412
- let archived = 0;
413
- for (const e of entries) {
414
- if (e.op === "tombstone") {
415
- // Explicit committed decision — see the class comment above.
416
- tombstone.run(e.entry_id, this.repoId);
417
- archived += lastChanges.get().c;
418
- continue;
419
- }
420
- const existing = findMemory.get(e.entry_id, this.repoId);
421
- if (existing)
422
- continue;
423
- insertMemory.run(uuidv7(), e.type, e.statement,
424
- // memories.scope is CHECK-constrained to project/session; a
425
- // path-prefix scope from the file degrades to 'project'.
426
- e.scope === "project" || e.scope === "session" ? e.scope : "project", deriveConfidence(e), this.projectId, e.evidence, e.recurrence, this.repoId, e.entry_id);
427
- }
428
- return archived;
429
- }
430
- /**
431
- * Upsert every entry into shared_entries keyed on (repo_id,
432
- * entry_id) — the UNIQUE index from migration 009 makes this an
433
- * upsert rather than an append. `confidence` is the DERIVED value
434
- * (deriveConfidence), never a transported float (plan §5.3).
435
- * Returns the number of rows inserted or updated.
436
- */
437
- upsertEntries(entries) {
438
- const upsert = this.store.prepare(`INSERT INTO shared_entries
439
- (id, repo_id, entry_id, type, statement, scope, confidence,
440
- evidence, origin, author_hash, op, supersedes, created_at,
441
- imported_at)
442
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
443
- ON CONFLICT(repo_id, entry_id) DO UPDATE SET
444
- type = excluded.type,
445
- statement = excluded.statement,
446
- scope = excluded.scope,
447
- confidence = excluded.confidence,
448
- evidence = excluded.evidence,
449
- origin = excluded.origin,
450
- author_hash = excluded.author_hash,
451
- op = excluded.op,
452
- supersedes = excluded.supersedes,
453
- created_at = excluded.created_at,
454
- imported_at = datetime('now')`);
455
- let n = 0;
456
- for (const e of entries) {
457
- upsert.run(uuidv7(), this.repoId, e.entry_id, e.type, e.statement, e.scope, deriveConfidence(e), e.evidence, e.origin, e.author_hash, e.op, e.supersedes, e.created_at);
458
- n++;
459
- }
460
- return n;
461
- }
462
- }
@@ -1,29 +0,0 @@
1
- import { type SqliteAdapter } from "./sqlite-adapter.js";
2
- export interface StoreOptions {
3
- path: string;
4
- }
5
- /**
6
- * Thin generic wrapper around {@link SqliteAdapter}. The Store deliberately
7
- * does NOT pre-prepare statements: callers (MemoryService, ToolCallObserver,
8
- * Reflector, Retrospective, Metrics, …) prepare SQL on demand so that any
9
- * migration can add columns/tables without requiring coordinated edits here.
10
- *
11
- * v0.2.0 (migration 003_v02_signal.sql) introduces the `kevin_metrics` and
12
- * `kevin_settings` tables plus nullable columns on `memories` and
13
- * `tool_calls`. All v0.2.0 callers read/write those rows via the existing
14
- * {@link Store.prepare | prepare()} / {@link Store.exec | exec()} /
15
- * {@link Store.transaction | transaction()} surface — no new member is
16
- * required on Store. K2-005 confirms this contract by exercising the new
17
- * tables/columns through the generic helpers (see
18
- * `tests/unit/store-prepare-003.test.ts`).
19
- */
20
- export declare class Store {
21
- private db;
22
- private closed;
23
- constructor(options: StoreOptions);
24
- prepare(sql: string): ReturnType<SqliteAdapter["prepare"]>;
25
- transaction<T>(fn: () => T): T;
26
- exec(sql: string): void;
27
- close(): void;
28
- get raw(): SqliteAdapter;
29
- }
@@ -1,50 +0,0 @@
1
- import { createDatabase } from "./sqlite-adapter.js";
2
- /**
3
- * Thin generic wrapper around {@link SqliteAdapter}. The Store deliberately
4
- * does NOT pre-prepare statements: callers (MemoryService, ToolCallObserver,
5
- * Reflector, Retrospective, Metrics, …) prepare SQL on demand so that any
6
- * migration can add columns/tables without requiring coordinated edits here.
7
- *
8
- * v0.2.0 (migration 003_v02_signal.sql) introduces the `kevin_metrics` and
9
- * `kevin_settings` tables plus nullable columns on `memories` and
10
- * `tool_calls`. All v0.2.0 callers read/write those rows via the existing
11
- * {@link Store.prepare | prepare()} / {@link Store.exec | exec()} /
12
- * {@link Store.transaction | transaction()} surface — no new member is
13
- * required on Store. K2-005 confirms this contract by exercising the new
14
- * tables/columns through the generic helpers (see
15
- * `tests/unit/store-prepare-003.test.ts`).
16
- */
17
- export class Store {
18
- db;
19
- closed = false;
20
- constructor(options) {
21
- this.db = createDatabase(options.path);
22
- this.db.exec("PRAGMA journal_mode = WAL");
23
- this.db.exec("PRAGMA foreign_keys = ON");
24
- }
25
- prepare(sql) {
26
- if (this.closed)
27
- throw new Error("Store is closed");
28
- return this.db.prepare(sql);
29
- }
30
- transaction(fn) {
31
- if (this.closed)
32
- throw new Error("Store is closed");
33
- const tx = this.db.transaction(fn);
34
- return tx();
35
- }
36
- exec(sql) {
37
- if (this.closed)
38
- throw new Error("Store is closed");
39
- this.db.exec(sql);
40
- }
41
- close() {
42
- if (this.closed)
43
- return;
44
- this.closed = true;
45
- this.db.close();
46
- }
47
- get raw() {
48
- return this.db;
49
- }
50
- }
@@ -1,32 +0,0 @@
1
- import type { Store } from "./Store.js";
2
- import type { Metrics } from "./metrics.js";
3
- export interface ToolExecuteInput {
4
- tool: string;
5
- args?: Record<string, unknown>;
6
- agent?: string;
7
- sessionId?: string;
8
- callID?: string;
9
- projectId?: string | null;
10
- }
11
- export interface ToolExecuteOutput {
12
- success?: boolean;
13
- stdout?: string;
14
- stderr?: string;
15
- exitCode?: number;
16
- }
17
- export declare class ToolCallObserver {
18
- private store;
19
- private startTs;
20
- private readonly metrics;
21
- constructor(store: Store, metrics?: Metrics | null);
22
- onBefore(input: ToolExecuteInput, _output: ToolExecuteOutput): void;
23
- onAfter(input: ToolExecuteInput, output: ToolExecuteOutput): void;
24
- private isDedupEnabled;
25
- redactSecrets(text: string): string;
26
- summarizeArgs(args: Record<string, unknown>): string;
27
- inferErrorType(stderr: string, stdout: string, exitCode?: number): string;
28
- private key;
29
- private looksSecret;
30
- private redactArgs;
31
- private redactValue;
32
- }