@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.
- package/dist/plugin/index.d.ts +4 -3
- package/dist/plugin/index.js +122 -113
- package/dist/plugin/native.d.ts +2 -2
- package/dist/plugin/native.js +2 -2
- package/package.json +56 -68
- package/LICENSE +0 -21
- package/README.md +0 -605
- package/dist/migrations/001_initial.sql +0 -92
- package/dist/migrations/002_indexes.sql +0 -14
- package/dist/migrations/003_v02_signal.sql +0 -58
- package/dist/migrations/004_v03_knowledge.sql +0 -138
- package/dist/migrations/005_v04_signal.sql +0 -57
- package/dist/migrations/006_v05_glassbox.sql +0 -118
- package/dist/migrations/007_v06_pull.sql +0 -145
- package/dist/migrations/008_v07_truth.sql +0 -124
- package/dist/migrations/009_v08_team.sql +0 -100
- package/dist/migrations/010_v09_native.sql +0 -78
- package/dist/migrations/011_v10_proven.sql +0 -69
- package/dist/migrations/012_v11_drift.sql +0 -24
- package/dist/plugin/Archiver.d.ts +0 -42
- package/dist/plugin/Archiver.js +0 -83
- package/dist/plugin/ArtifactWriter.d.ts +0 -93
- package/dist/plugin/ArtifactWriter.js +0 -291
- package/dist/plugin/CausalChain.d.ts +0 -22
- package/dist/plugin/CausalChain.js +0 -197
- package/dist/plugin/ChatBridge.d.ts +0 -41
- package/dist/plugin/ChatBridge.js +0 -103
- package/dist/plugin/ConflictDetector.d.ts +0 -35
- package/dist/plugin/ConflictDetector.js +0 -260
- package/dist/plugin/ContextInjector.d.ts +0 -222
- package/dist/plugin/ContextInjector.js +0 -533
- package/dist/plugin/ConventionMiner.d.ts +0 -35
- package/dist/plugin/ConventionMiner.js +0 -242
- package/dist/plugin/Curator.d.ts +0 -116
- package/dist/plugin/Curator.js +0 -324
- package/dist/plugin/DashboardHtml.d.ts +0 -5
- package/dist/plugin/DashboardHtml.js +0 -180
- package/dist/plugin/Feedback.d.ts +0 -67
- package/dist/plugin/Feedback.js +0 -120
- package/dist/plugin/HookLiveness.d.ts +0 -88
- package/dist/plugin/HookLiveness.js +0 -322
- package/dist/plugin/InjectionLedger.d.ts +0 -98
- package/dist/plugin/InjectionLedger.js +0 -295
- package/dist/plugin/LessonFixer.d.ts +0 -44
- package/dist/plugin/LessonFixer.js +0 -45
- package/dist/plugin/Materializer.d.ts +0 -83
- package/dist/plugin/Materializer.js +0 -181
- package/dist/plugin/MemoryService.d.ts +0 -360
- package/dist/plugin/MemoryService.js +0 -1242
- package/dist/plugin/Migrate.d.ts +0 -17
- package/dist/plugin/Migrate.js +0 -271
- package/dist/plugin/PatternMiner.d.ts +0 -49
- package/dist/plugin/PatternMiner.js +0 -132
- package/dist/plugin/QualityGate.d.ts +0 -124
- package/dist/plugin/QualityGate.js +0 -119
- package/dist/plugin/Reflector.d.ts +0 -91
- package/dist/plugin/Reflector.js +0 -307
- package/dist/plugin/RepoIdentity.d.ts +0 -124
- package/dist/plugin/RepoIdentity.js +0 -300
- package/dist/plugin/RepoTruth.d.ts +0 -80
- package/dist/plugin/RepoTruth.js +0 -599
- package/dist/plugin/Retrospective.d.ts +0 -17
- package/dist/plugin/Retrospective.js +0 -216
- package/dist/plugin/SharedLayer.d.ts +0 -159
- package/dist/plugin/SharedLayer.js +0 -462
- package/dist/plugin/Store.d.ts +0 -29
- package/dist/plugin/Store.js +0 -50
- package/dist/plugin/ToolCallObserver.d.ts +0 -32
- package/dist/plugin/ToolCallObserver.js +0 -201
- package/dist/plugin/TuiActions.d.ts +0 -43
- package/dist/plugin/TuiActions.js +0 -181
- package/dist/plugin/TuiSnapshots.d.ts +0 -24
- package/dist/plugin/TuiSnapshots.js +0 -158
- package/dist/plugin/columns.d.ts +0 -11
- package/dist/plugin/columns.js +0 -54
- package/dist/plugin/confidence.d.ts +0 -8
- package/dist/plugin/confidence.js +0 -34
- package/dist/plugin/contract.d.ts +0 -54
- package/dist/plugin/contract.js +0 -386
- package/dist/plugin/diff.d.ts +0 -8
- package/dist/plugin/diff.js +0 -182
- package/dist/plugin/escape.d.ts +0 -39
- package/dist/plugin/escape.js +0 -60
- package/dist/plugin/fingerprint.d.ts +0 -27
- package/dist/plugin/fingerprint.js +0 -73
- package/dist/plugin/inferability.d.ts +0 -32
- package/dist/plugin/inferability.js +0 -88
- package/dist/plugin/kevin_approve.d.ts +0 -34
- package/dist/plugin/kevin_approve.js +0 -52
- package/dist/plugin/kevin_audit.d.ts +0 -229
- package/dist/plugin/kevin_audit.js +0 -631
- package/dist/plugin/kevin_bench.d.ts +0 -10
- package/dist/plugin/kevin_bench.js +0 -82
- package/dist/plugin/kevin_conflicts.d.ts +0 -9
- package/dist/plugin/kevin_conflicts.js +0 -50
- package/dist/plugin/kevin_contract.d.ts +0 -8
- package/dist/plugin/kevin_contract.js +0 -59
- package/dist/plugin/kevin_doctor.d.ts +0 -62
- package/dist/plugin/kevin_doctor.js +0 -196
- package/dist/plugin/kevin_facts.d.ts +0 -42
- package/dist/plugin/kevin_facts.js +0 -36
- package/dist/plugin/kevin_forget.d.ts +0 -33
- package/dist/plugin/kevin_forget.js +0 -260
- package/dist/plugin/kevin_native.d.ts +0 -29
- package/dist/plugin/kevin_native.js +0 -79
- package/dist/plugin/kevin_propose.d.ts +0 -23
- package/dist/plugin/kevin_propose.js +0 -14
- package/dist/plugin/kevin_publish.d.ts +0 -38
- package/dist/plugin/kevin_publish.js +0 -18
- package/dist/plugin/kevin_why.d.ts +0 -23
- package/dist/plugin/kevin_why.js +0 -111
- package/dist/plugin/memory-format.d.ts +0 -20
- package/dist/plugin/memory-format.js +0 -59
- package/dist/plugin/metrics.d.ts +0 -97
- package/dist/plugin/metrics.js +0 -288
- package/dist/plugin/okf-export.d.ts +0 -3
- package/dist/plugin/okf-export.js +0 -134
- package/dist/plugin/okf-import.d.ts +0 -76
- package/dist/plugin/okf-import.js +0 -271
- package/dist/plugin/okf.d.ts +0 -107
- package/dist/plugin/okf.js +0 -303
- package/dist/plugin/perf.d.ts +0 -35
- package/dist/plugin/perf.js +0 -174
- package/dist/plugin/query-tokenizer.d.ts +0 -13
- package/dist/plugin/query-tokenizer.js +0 -133
- package/dist/plugin/redact.d.ts +0 -2
- package/dist/plugin/redact.js +0 -18
- package/dist/plugin/replay-types.d.ts +0 -69
- package/dist/plugin/replay-types.js +0 -156
- package/dist/plugin/replay.d.ts +0 -36
- package/dist/plugin/replay.js +0 -202
- package/dist/plugin/sqlite-adapter.d.ts +0 -12
- package/dist/plugin/sqlite-adapter.js +0 -81
- package/dist/plugin/time-ms.d.ts +0 -1
- package/dist/plugin/time-ms.js +0 -16
- package/dist/plugin/tui-types.d.ts +0 -59
- package/dist/plugin/tui-types.js +0 -4
- package/dist/plugin/tui.d.ts +0 -18
- package/dist/plugin/tui.js +0 -198
- package/dist/plugin/uuid.d.ts +0 -1
- 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
|
-
}
|
package/dist/plugin/Store.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/plugin/Store.js
DELETED
|
@@ -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
|
-
}
|