@jmtrin/opencode-kevin 1.2.0 → 1.4.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 (140) hide show
  1. package/README.md +84 -22
  2. package/dist/plugin/index.d.ts +5 -4
  3. package/dist/plugin/index.js +127 -113
  4. package/dist/plugin/native.d.ts +2 -2
  5. package/dist/plugin/native.js +2 -2
  6. package/package.json +56 -68
  7. package/dist/migrations/001_initial.sql +0 -92
  8. package/dist/migrations/002_indexes.sql +0 -14
  9. package/dist/migrations/003_v02_signal.sql +0 -58
  10. package/dist/migrations/004_v03_knowledge.sql +0 -138
  11. package/dist/migrations/005_v04_signal.sql +0 -57
  12. package/dist/migrations/006_v05_glassbox.sql +0 -118
  13. package/dist/migrations/007_v06_pull.sql +0 -145
  14. package/dist/migrations/008_v07_truth.sql +0 -124
  15. package/dist/migrations/009_v08_team.sql +0 -100
  16. package/dist/migrations/010_v09_native.sql +0 -78
  17. package/dist/migrations/011_v10_proven.sql +0 -69
  18. package/dist/migrations/012_v11_drift.sql +0 -24
  19. package/dist/plugin/Archiver.d.ts +0 -42
  20. package/dist/plugin/Archiver.js +0 -83
  21. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  22. package/dist/plugin/ArtifactWriter.js +0 -291
  23. package/dist/plugin/CausalChain.d.ts +0 -22
  24. package/dist/plugin/CausalChain.js +0 -197
  25. package/dist/plugin/ChatBridge.d.ts +0 -41
  26. package/dist/plugin/ChatBridge.js +0 -103
  27. package/dist/plugin/ConflictDetector.d.ts +0 -35
  28. package/dist/plugin/ConflictDetector.js +0 -260
  29. package/dist/plugin/ContextInjector.d.ts +0 -222
  30. package/dist/plugin/ContextInjector.js +0 -533
  31. package/dist/plugin/ConventionMiner.d.ts +0 -35
  32. package/dist/plugin/ConventionMiner.js +0 -242
  33. package/dist/plugin/Curator.d.ts +0 -116
  34. package/dist/plugin/Curator.js +0 -324
  35. package/dist/plugin/DashboardHtml.d.ts +0 -5
  36. package/dist/plugin/DashboardHtml.js +0 -180
  37. package/dist/plugin/Feedback.d.ts +0 -67
  38. package/dist/plugin/Feedback.js +0 -120
  39. package/dist/plugin/HookLiveness.d.ts +0 -88
  40. package/dist/plugin/HookLiveness.js +0 -322
  41. package/dist/plugin/InjectionLedger.d.ts +0 -98
  42. package/dist/plugin/InjectionLedger.js +0 -295
  43. package/dist/plugin/LessonFixer.d.ts +0 -44
  44. package/dist/plugin/LessonFixer.js +0 -45
  45. package/dist/plugin/Materializer.d.ts +0 -83
  46. package/dist/plugin/Materializer.js +0 -181
  47. package/dist/plugin/MemoryService.d.ts +0 -360
  48. package/dist/plugin/MemoryService.js +0 -1242
  49. package/dist/plugin/Migrate.d.ts +0 -17
  50. package/dist/plugin/Migrate.js +0 -271
  51. package/dist/plugin/PatternMiner.d.ts +0 -49
  52. package/dist/plugin/PatternMiner.js +0 -132
  53. package/dist/plugin/QualityGate.d.ts +0 -124
  54. package/dist/plugin/QualityGate.js +0 -119
  55. package/dist/plugin/Reflector.d.ts +0 -91
  56. package/dist/plugin/Reflector.js +0 -307
  57. package/dist/plugin/RepoIdentity.d.ts +0 -124
  58. package/dist/plugin/RepoIdentity.js +0 -300
  59. package/dist/plugin/RepoTruth.d.ts +0 -80
  60. package/dist/plugin/RepoTruth.js +0 -599
  61. package/dist/plugin/Retrospective.d.ts +0 -17
  62. package/dist/plugin/Retrospective.js +0 -216
  63. package/dist/plugin/SharedLayer.d.ts +0 -159
  64. package/dist/plugin/SharedLayer.js +0 -462
  65. package/dist/plugin/Store.d.ts +0 -29
  66. package/dist/plugin/Store.js +0 -50
  67. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  68. package/dist/plugin/ToolCallObserver.js +0 -201
  69. package/dist/plugin/TuiActions.d.ts +0 -43
  70. package/dist/plugin/TuiActions.js +0 -181
  71. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  72. package/dist/plugin/TuiSnapshots.js +0 -158
  73. package/dist/plugin/columns.d.ts +0 -11
  74. package/dist/plugin/columns.js +0 -54
  75. package/dist/plugin/confidence.d.ts +0 -8
  76. package/dist/plugin/confidence.js +0 -34
  77. package/dist/plugin/contract.d.ts +0 -54
  78. package/dist/plugin/contract.js +0 -386
  79. package/dist/plugin/diff.d.ts +0 -8
  80. package/dist/plugin/diff.js +0 -182
  81. package/dist/plugin/escape.d.ts +0 -39
  82. package/dist/plugin/escape.js +0 -60
  83. package/dist/plugin/fingerprint.d.ts +0 -27
  84. package/dist/plugin/fingerprint.js +0 -73
  85. package/dist/plugin/inferability.d.ts +0 -32
  86. package/dist/plugin/inferability.js +0 -88
  87. package/dist/plugin/kevin_approve.d.ts +0 -34
  88. package/dist/plugin/kevin_approve.js +0 -52
  89. package/dist/plugin/kevin_audit.d.ts +0 -229
  90. package/dist/plugin/kevin_audit.js +0 -631
  91. package/dist/plugin/kevin_bench.d.ts +0 -10
  92. package/dist/plugin/kevin_bench.js +0 -82
  93. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  94. package/dist/plugin/kevin_conflicts.js +0 -50
  95. package/dist/plugin/kevin_contract.d.ts +0 -8
  96. package/dist/plugin/kevin_contract.js +0 -59
  97. package/dist/plugin/kevin_doctor.d.ts +0 -62
  98. package/dist/plugin/kevin_doctor.js +0 -196
  99. package/dist/plugin/kevin_facts.d.ts +0 -42
  100. package/dist/plugin/kevin_facts.js +0 -36
  101. package/dist/plugin/kevin_forget.d.ts +0 -33
  102. package/dist/plugin/kevin_forget.js +0 -260
  103. package/dist/plugin/kevin_native.d.ts +0 -29
  104. package/dist/plugin/kevin_native.js +0 -79
  105. package/dist/plugin/kevin_propose.d.ts +0 -23
  106. package/dist/plugin/kevin_propose.js +0 -14
  107. package/dist/plugin/kevin_publish.d.ts +0 -38
  108. package/dist/plugin/kevin_publish.js +0 -18
  109. package/dist/plugin/kevin_why.d.ts +0 -23
  110. package/dist/plugin/kevin_why.js +0 -111
  111. package/dist/plugin/memory-format.d.ts +0 -20
  112. package/dist/plugin/memory-format.js +0 -59
  113. package/dist/plugin/metrics.d.ts +0 -97
  114. package/dist/plugin/metrics.js +0 -288
  115. package/dist/plugin/okf-export.d.ts +0 -3
  116. package/dist/plugin/okf-export.js +0 -134
  117. package/dist/plugin/okf-import.d.ts +0 -76
  118. package/dist/plugin/okf-import.js +0 -271
  119. package/dist/plugin/okf.d.ts +0 -107
  120. package/dist/plugin/okf.js +0 -303
  121. package/dist/plugin/perf.d.ts +0 -35
  122. package/dist/plugin/perf.js +0 -174
  123. package/dist/plugin/query-tokenizer.d.ts +0 -13
  124. package/dist/plugin/query-tokenizer.js +0 -133
  125. package/dist/plugin/redact.d.ts +0 -2
  126. package/dist/plugin/redact.js +0 -18
  127. package/dist/plugin/replay-types.d.ts +0 -69
  128. package/dist/plugin/replay-types.js +0 -156
  129. package/dist/plugin/replay.d.ts +0 -36
  130. package/dist/plugin/replay.js +0 -202
  131. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  132. package/dist/plugin/sqlite-adapter.js +0 -81
  133. package/dist/plugin/time-ms.d.ts +0 -1
  134. package/dist/plugin/time-ms.js +0 -16
  135. package/dist/plugin/tui-types.d.ts +0 -59
  136. package/dist/plugin/tui-types.js +0 -4
  137. package/dist/plugin/tui.d.ts +0 -18
  138. package/dist/plugin/tui.js +0 -198
  139. package/dist/plugin/uuid.d.ts +0 -1
  140. package/dist/plugin/uuid.js +0 -50
@@ -1,242 +0,0 @@
1
- import { fingerprint } from "./fingerprint.js";
2
- const DEFAULT_MIN_SUPPORT = 5;
3
- export class ConventionMiner {
4
- store;
5
- memoryService;
6
- projectId;
7
- metrics;
8
- constructor(store, memoryService, projectId, metrics) {
9
- this.store = store;
10
- this.memoryService = memoryService;
11
- this.projectId = projectId;
12
- this.metrics = metrics ?? null;
13
- // `memoryService` is retained for emission (K7-012); mining itself is
14
- // store-only.
15
- void this.memoryService;
16
- }
17
- /** v0.7.0 (K7-010) — the `sequence` miner over successful tool_calls. */
18
- mineSequence(minSupport = DEFAULT_MIN_SUPPORT) {
19
- const rows = this.fetchToolCalls();
20
- if (rows.length === 0)
21
- return [];
22
- // Group successful calls by session in execution order (ts ASC, then id
23
- // as a deterministic tie-break).
24
- const bySession = new Map();
25
- const sorted = [...rows]
26
- .filter((r) => r.success === 1)
27
- .sort((a, b) => a.session_id === b.session_id
28
- ? compareTs(a.ts, b.ts) || a.id.localeCompare(b.id)
29
- : a.session_id.localeCompare(b.session_id));
30
- for (const r of sorted) {
31
- const list = bySession.get(r.session_id) ?? [];
32
- list.push(r);
33
- bySession.set(r.session_id, list);
34
- }
35
- const map = new Map();
36
- const record = (key, statement, sessionId) => {
37
- let cand = map.get(key);
38
- if (!cand) {
39
- cand = { key, statement, sessions: new Set() };
40
- map.set(key, cand);
41
- }
42
- cand.sessions.add(sessionId);
43
- };
44
- for (const [sessionId, list] of bySession) {
45
- if (list.length < 2)
46
- continue;
47
- for (let i = 0; i < list.length - 1; i++) {
48
- const a = list[i];
49
- const b = list[i + 1];
50
- const aTok = tokenOf(a);
51
- const bTok = tokenOf(b);
52
- const key2 = `seq::${aTok}::${bTok}`;
53
- const stmt2 = statementForSequence(aTok, bTok);
54
- record(key2, stmt2, sessionId);
55
- if (i + 2 < list.length) {
56
- const c = list[i + 2];
57
- const cTok = tokenOf(c);
58
- const key3 = `seq::${aTok}::${bTok}::${cTok}`;
59
- const stmt3 = statementForSequence3(aTok, bTok, cTok);
60
- record(key3, stmt3, sessionId);
61
- }
62
- }
63
- }
64
- return this.materialize(map, minSupport, "sequence");
65
- }
66
- /** v0.7.0 (K7-011) — the `co_edit` miner over same-session file writes. */
67
- mineCoEdit(minSupport = DEFAULT_MIN_SUPPORT) {
68
- const rows = this.fetchToolCalls();
69
- if (rows.length === 0)
70
- return [];
71
- // Successful file writes (broad: any success call mentioning a path) per
72
- // session. Pairs span two different directory prefixes.
73
- const bySession = new Map();
74
- const sorted = [...rows]
75
- .filter((r) => r.success === 1 && pathOf(r) !== null)
76
- .sort((a, b) => a.session_id === b.session_id
77
- ? compareTs(a.ts, b.ts) || a.id.localeCompare(b.id)
78
- : a.session_id.localeCompare(b.session_id));
79
- for (const r of sorted) {
80
- const list = bySession.get(r.session_id) ?? [];
81
- list.push(pathOf(r));
82
- bySession.set(r.session_id, list);
83
- }
84
- const map = new Map();
85
- const record = (key, statement, sessionId) => {
86
- let cand = map.get(key);
87
- if (!cand) {
88
- cand = { key, statement, sessions: new Set() };
89
- map.set(key, cand);
90
- }
91
- cand.sessions.add(sessionId);
92
- };
93
- for (const [sessionId, paths] of bySession) {
94
- const unique = [...new Set(paths)];
95
- if (unique.length < 2)
96
- continue;
97
- // Bounded enumeration: a large session must not explode into an
98
- // O(n²) pair set. Cap distinct prefixes per session at 40.
99
- const limit = Math.min(unique.length, 40);
100
- for (let i = 0; i < limit; i++) {
101
- const pA = unique[i];
102
- for (let j = i + 1; j < limit; j++) {
103
- const pB = unique[j];
104
- const prefixA = dirOf(pA);
105
- const prefixB = dirOf(pB);
106
- if (prefixA === prefixB)
107
- continue;
108
- const [lo, hi] = prefixA < prefixB ? [prefixA, prefixB] : [prefixB, prefixA];
109
- const key = `coedit::${lo}::${hi}`;
110
- const stmt = `every new file under ${lo}/ is accompanied by a change under ${hi}/`;
111
- record(key, stmt, sessionId);
112
- }
113
- }
114
- }
115
- return this.materialize(map, minSupport, "co_edit");
116
- }
117
- /** v0.7.0 (K7-012) — mine both kinds in one call. */
118
- mine(minSupport = DEFAULT_MIN_SUPPORT) {
119
- return [
120
- ...this.mineSequence(minSupport),
121
- ...this.mineCoEdit(minSupport),
122
- ].sort((a, b) => a.statement.localeCompare(b.statement));
123
- }
124
- /**
125
- * v0.7.0 (K7-012 / plan §5.4, D7-11) — emit mined(conventions) as `rule`
126
- * memories. Returns the number of memories created or refreshed and
127
- * increments `conventions_mined`. Runs on session.idle only, behind
128
- * convention_mining_enabled. The fingerprint derives from the statement,
129
- * so a re-mine of an unchanged convention collides and supersedes.
130
- */
131
- emit(conventions) {
132
- let emitted = 0;
133
- for (const c of conventions) {
134
- const derivedFingerprint = fingerprint(c.statement, this.projectId);
135
- // save() derives the fingerprint-based supersede/collision path for
136
- // `rule` types automatically, so re-emitting an unchanged convention
137
- // refreshes rather than duplicates.
138
- this.memoryService.save({
139
- type: "rule",
140
- origin: "pattern",
141
- scope: "project",
142
- projectId: this.projectId,
143
- fingerprint: derivedFingerprint,
144
- content: c.statement,
145
- relevanceScore: 0.5,
146
- sourceTool: "ConventionMiner",
147
- });
148
- this.metrics?.incr("conventions_mined", 1);
149
- emitted += 1;
150
- }
151
- return emitted;
152
- }
153
- materialize(map, minSupport, kind) {
154
- const out = [];
155
- for (const cand of map.values()) {
156
- if (cand.sessions.size < minSupport)
157
- continue;
158
- out.push({
159
- fingerprint: fingerprint(cand.statement, this.projectId),
160
- statement: cand.statement,
161
- support: cand.sessions.size,
162
- kind,
163
- });
164
- }
165
- // Deterministic order: lexicographic by statement (ties are resolved).
166
- return out.sort((a, b) => a.statement.localeCompare(b.statement));
167
- }
168
- fetchToolCalls() {
169
- const rows = this.store
170
- .prepare(`SELECT id, session_id, ts, tool, success, args_summary
171
- FROM tool_calls
172
- WHERE project_id = ?
173
- ORDER BY session_id, ts`)
174
- .all(this.projectId);
175
- return rows;
176
- }
177
- }
178
- // ---------------------------------------------------------------------------
179
- // Helpers — deterministic token/path extraction. Every value is normalized so
180
- // the same underlying fact never collides via a JavaScript type difference.
181
- // ---------------------------------------------------------------------------
182
- /** `<tool>:<first path segment>` token for sequence mining. */
183
- function tokenOf(r) {
184
- const seg = firstPathSegment(r.args_summary);
185
- return `${r.tool}:${seg}`;
186
- }
187
- function compareTs(a, b) {
188
- return a.localeCompare(b);
189
- }
190
- /** First path segment of the first path-like argument, else "*". */
191
- function firstPathSegment(argsSummary) {
192
- const paths = pathSegments(argsSummary);
193
- const first = paths[0];
194
- if (!first)
195
- return "*";
196
- return first.split("/").filter(Boolean)[0] ?? "*";
197
- }
198
- /** All path segments found in args_summary, in appearance order. */
199
- function pathSegments(argsSummary) {
200
- const text = argsSummary ?? "";
201
- const found = [];
202
- // Try JSON object string values first.
203
- try {
204
- const parsed = JSON.parse(text);
205
- if (typeof parsed === "object" &&
206
- parsed !== null &&
207
- !Array.isArray(parsed)) {
208
- for (const v of Object.values(parsed)) {
209
- if (typeof v === "string" && /\//.test(v) && !v.includes(" ")) {
210
- found.push(v.replace(/\\/g, "/").toLowerCase().trim());
211
- }
212
- }
213
- }
214
- }
215
- catch {
216
- /* not JSON */
217
- }
218
- if (found.length === 0) {
219
- const m = text.match(/[^\s"':,=]*\/[^\s"':,=]*/g);
220
- for (const tok of m ?? []) {
221
- found.push(tok.replace(/\\/g, "/").toLowerCase().trim());
222
- }
223
- }
224
- return found;
225
- }
226
- /** Directory of a path (drop the filename). `src/routes/user.ts` → `src/routes`. */
227
- function dirOf(p) {
228
- const segs = p.split("/").filter(Boolean);
229
- segs.pop();
230
- return segs.join("/") || "*";
231
- }
232
- /** A deterministic path for a call if it is a file write/read on disk. */
233
- function pathOf(r) {
234
- const segs = pathSegments(r.args_summary);
235
- return segs[0] ?? null;
236
- }
237
- function statementForSequence(a, b) {
238
- return `every ${a} is immediately followed by ${b}`;
239
- }
240
- function statementForSequence3(a, b, c) {
241
- return `every ${a} is followed by ${b} and then ${c}`;
242
- }
@@ -1,116 +0,0 @@
1
- import { type ArtifactWriter } from "./ArtifactWriter.js";
2
- import type { MemoryService } from "./MemoryService.js";
3
- import type { Store } from "./Store.js";
4
- import type { Metrics } from "./metrics.js";
5
- /**
6
- * K6-012/013 — v0.6.0 pull — candidate selection, line rendering and the
7
- * proposal lifecycle (plan §5.4/§5.5).
8
- *
9
- * Deliberately fs-free: this module holds NO filesystem capability — reads
10
- * and writes are delegated to the `ArtifactWriter` at the call site (D6-01).
11
- */
12
- export interface CurationCandidate {
13
- readonly memoryId: string;
14
- /** The single AGENTS.md bullet. */
15
- readonly line: string;
16
- readonly confidence: number;
17
- /** e.g. "verified 3×, last 2026-08-04". */
18
- readonly evidence: string;
19
- }
20
- export type ProposalKind = "agents_md" | "skill" | "reference";
21
- export type ProposalStatus = "pending" | "approved" | "rejected" | "applied" | "superseded";
22
- export type ProposalTransition = "approve" | "reject" | "apply" | "supersede";
23
- /** Plan §5.4 — the row shape of §6's `curation_proposals` table, surfaced. */
24
- export interface CurationProposal {
25
- readonly id: string;
26
- readonly kind: ProposalKind;
27
- readonly targetPath: string;
28
- readonly memoryIds: readonly string[];
29
- readonly proposedText: string;
30
- readonly diff: string;
31
- readonly status: ProposalStatus;
32
- readonly createdAt: string;
33
- }
34
- /** v0.8.0 (K8-023 / plan §5.7) — the candidate source. */
35
- export type CandidateSource = "memories" | "shared";
36
- export declare function firstSentence(content: string): string;
37
- export declare class Curator {
38
- private readonly store;
39
- private readonly memoryService;
40
- private readonly projectId;
41
- private readonly repoId?;
42
- private readonly metrics;
43
- constructor(store: Store, memoryService: MemoryService, projectId: string, metrics?: Metrics | null, repoId?: string | null | undefined);
44
- /**
45
- * v0.8.0 (K8-023 / plan §5.7) — the source the flag selects. When
46
- * `shared_layer_enabled` is on, curation reads the committed OKF file's
47
- * projection (`shared_entries`) instead of the local `memories` table.
48
- * The routing lives here, next to the predicate it switches, so a
49
- * reviewer sees substrate and rendering change together (D8-11).
50
- */
51
- private sourceFromFlag;
52
- /** v0.8.0 (K8-023) — the shared-layer candidate rows. */
53
- private sharedRows;
54
- /**
55
- * Plan §5.4 selection predicate — all clauses must hold, evaluated
56
- * verbatim. The confidence floor cannot be expressed in SQL (it is the
57
- * two-sided formula of `computeConfidence`), so the cheap clauses run
58
- * in SQL and confidence is computed and filtered per row in JS.
59
- *
60
- * The floor is `confidence >= 0.6` where the feedback arm of D6-09 also
61
- * clears it: `computeConfidence` credits one positive human verdict at
62
- * 0.05 (K5-010), so a row with `feedback_positive = 1` and no causal
63
- * evidence scores 0.55 — yet D6-09's warrant is "either the world
64
- * verified it twice, or a human verified it once". The disjunction
65
- * (K6-012 acceptance) is the floor's complement for the feedback arm.
66
- *
67
- * Ordered by `confidence DESC, updated_at DESC`, capped at 20 lines and
68
- * 4000 characters of content, whichever binds first. The char budget
69
- * counts the raw content length: rendered lines are truncated to 160
70
- * chars, so a rendered-line budget could never bind before the line cap.
71
- *
72
- * v0.8.0 (K8-023 / plan §5.7) — `source` selects the substrate: the
73
- * v0.6.0 path over `memories` ("local") or the shared layer. The
74
- * predicate, the caps and the deterministic sort are unchanged in both
75
- * cases — only the FROM changes. The shared schema has no feedback
76
- * columns, so the feedback disjunct of the predicate can never fire
77
- * there; `evidence >= 2` is its SQL half, evaluated in the query.
78
- */
79
- candidates(limit?: number, source?: CandidateSource): CurationCandidate[];
80
- /**
81
- * Renders the block sorted by memory id (D6-10): confidence orders
82
- * selection, id orders output. Adding one candidate to a set of ten
83
- * must change exactly one line of the rendered block.
84
- */
85
- renderBlock(candidates: CurationCandidate[]): string;
86
- /**
87
- * Plan §5.4 — strict dry run (D5-08): plan only, never write. Returns a
88
- * pending proposal per call, or [] when no candidate clears the floor.
89
- *
90
- * The proposal is a whole-block proposal: the persisted `proposed_text`
91
- * is the merged block (current block in the file + new candidate lines),
92
- * so approving it replaces the file block with exactly what was reviewed.
93
- * The current block is learned from `plan().before` — the Curator has no
94
- * fs capability, the writer reads (D6-01). Both plan calls are
95
- * deterministic, so a second propose() with unchanged inputs reproduces
96
- * the persisted diff byte-identically.
97
- *
98
- * A new proposal supersedes prior pending (or rejected) rows for the
99
- * same (project_id, kind, target_path) triple — §5.5's regeneration
100
- * arrow. Rows are never deleted (the audit trail is append-only).
101
- *
102
- * The schema's `memory_id` column is singular while a whole-block
103
- * proposal carries several ids; the contributing ids are stored joined
104
- * by "," (uuidv7 ids contain no comma) and split on read.
105
- */
106
- propose(kind: ProposalKind, writer: ArtifactWriter): CurationProposal[];
107
- /**
108
- * Plan §5.5 state machine, as explicit transitions with an exhaustive
109
- * switch — an unknown transition or an illegal source state throws
110
- * rather than silently no-oping. `decided_at` stamps approve/reject/
111
- * supersede, `applied_at` stamps apply.
112
- */
113
- transition(proposalId: string, transition: ProposalTransition): ProposalStatus;
114
- /** Plan §5.6 target paths. agents_md is a setting; skill/reference live under ~/.opencode-kevin. */
115
- private targetPathFor;
116
- }
@@ -1,324 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import { MARKER_BEGIN, MARKER_END, } from "./ArtifactWriter.js";
4
- import { computeConfidence } from "./confidence.js";
5
- import { normalize } from "./fingerprint.js";
6
- import { uuidv7 } from "./uuid.js";
7
- const MAX_CANDIDATE_LINES = 20;
8
- const MAX_CANDIDATE_CHARS = 4000;
9
- const CONFIDENCE_FLOOR = 0.6;
10
- const MAX_SENTENCE_CHARS = 160;
11
- export function firstSentence(content) {
12
- const match = content.match(/^[\s\S]*?(?=[.!?](?:\s|$)|\r?\n|$)/);
13
- const sentence = (match ? match[0] : content).trim();
14
- return sentence.slice(0, MAX_SENTENCE_CHARS);
15
- }
16
- function evidenceString(row) {
17
- const parts = [];
18
- if (row.evidence_count >= 2) {
19
- const last = row.last_verified_at
20
- ? `, last ${row.last_verified_at.slice(0, 10)}`
21
- : "";
22
- parts.push(`verified ${row.evidence_count}×${last}`);
23
- }
24
- if (row.feedback_positive >= 1) {
25
- parts.push(`feedback ${row.feedback_positive}×`);
26
- }
27
- return parts.join(", ");
28
- }
29
- export class Curator {
30
- store;
31
- memoryService;
32
- projectId;
33
- repoId;
34
- metrics;
35
- constructor(store, memoryService, projectId, metrics, repoId) {
36
- this.store = store;
37
- this.memoryService = memoryService;
38
- this.projectId = projectId;
39
- this.repoId = repoId;
40
- this.metrics = metrics ?? null;
41
- }
42
- /**
43
- * v0.8.0 (K8-023 / plan §5.7) — the source the flag selects. When
44
- * `shared_layer_enabled` is on, curation reads the committed OKF file's
45
- * projection (`shared_entries`) instead of the local `memories` table.
46
- * The routing lives here, next to the predicate it switches, so a
47
- * reviewer sees substrate and rendering change together (D8-11).
48
- */
49
- sourceFromFlag() {
50
- return this.memoryService.getSetting("shared_layer_enabled", "0") === "1"
51
- ? "shared"
52
- : "memories";
53
- }
54
- /** v0.8.0 (K8-023) — the shared-layer candidate rows. */
55
- sharedRows() {
56
- if (!this.repoId) {
57
- throw new Error("shared source requires a repoId");
58
- }
59
- return this.store
60
- .prepare(`SELECT entry_id AS id, statement AS content,
61
- evidence AS evidence_count,
62
- 0 AS recurrence_count,
63
- 0 AS feedback_positive,
64
- 0 AS feedback_negative,
65
- NULL AS last_verified_at,
66
- created_at AS updated_at
67
- FROM shared_entries
68
- WHERE repo_id = ?
69
- AND op = 'assert'
70
- AND evidence >= 2`)
71
- .all(this.repoId);
72
- }
73
- /**
74
- * Plan §5.4 selection predicate — all clauses must hold, evaluated
75
- * verbatim. The confidence floor cannot be expressed in SQL (it is the
76
- * two-sided formula of `computeConfidence`), so the cheap clauses run
77
- * in SQL and confidence is computed and filtered per row in JS.
78
- *
79
- * The floor is `confidence >= 0.6` where the feedback arm of D6-09 also
80
- * clears it: `computeConfidence` credits one positive human verdict at
81
- * 0.05 (K5-010), so a row with `feedback_positive = 1` and no causal
82
- * evidence scores 0.55 — yet D6-09's warrant is "either the world
83
- * verified it twice, or a human verified it once". The disjunction
84
- * (K6-012 acceptance) is the floor's complement for the feedback arm.
85
- *
86
- * Ordered by `confidence DESC, updated_at DESC`, capped at 20 lines and
87
- * 4000 characters of content, whichever binds first. The char budget
88
- * counts the raw content length: rendered lines are truncated to 160
89
- * chars, so a rendered-line budget could never bind before the line cap.
90
- *
91
- * v0.8.0 (K8-023 / plan §5.7) — `source` selects the substrate: the
92
- * v0.6.0 path over `memories` ("local") or the shared layer. The
93
- * predicate, the caps and the deterministic sort are unchanged in both
94
- * cases — only the FROM changes. The shared schema has no feedback
95
- * columns, so the feedback disjunct of the predicate can never fire
96
- * there; `evidence >= 2` is its SQL half, evaluated in the query.
97
- */
98
- candidates(limit, source = "memories") {
99
- const rows = source === "shared"
100
- ? this.sharedRows()
101
- : this.store
102
- .prepare(`SELECT id, content, evidence_count, recurrence_count,
103
- feedback_positive, feedback_negative,
104
- last_verified_at, updated_at
105
- FROM memories
106
- WHERE status = 'active'
107
- AND ignored = 0
108
- AND curated = 0
109
- AND (inferable IS NULL OR inferable != 1)
110
- AND (evidence_count >= 2 OR feedback_positive >= 1)`)
111
- .all();
112
- const scored = rows
113
- .map((row) => ({
114
- row,
115
- confidence: computeConfidence(row.evidence_count ?? 0, row.recurrence_count ?? 0, row.feedback_positive ?? 0, row.feedback_negative ?? 0),
116
- }))
117
- .filter((s) => s.confidence >= CONFIDENCE_FLOOR || s.row.feedback_positive >= 1)
118
- .sort((a, b) => b.confidence - a.confidence ||
119
- b.row.updated_at.localeCompare(a.row.updated_at));
120
- const maxLines = limit ?? MAX_CANDIDATE_LINES;
121
- const result = [];
122
- let totalChars = 0;
123
- for (const s of scored) {
124
- if (result.length >= maxLines)
125
- break;
126
- if (totalChars + s.row.content.length > MAX_CANDIDATE_CHARS)
127
- break;
128
- const evidence = evidenceString(s.row);
129
- result.push({
130
- memoryId: s.row.id,
131
- line: `- ${firstSentence(s.row.content)} (${evidence})`,
132
- confidence: s.confidence,
133
- evidence,
134
- });
135
- totalChars += s.row.content.length;
136
- }
137
- return result;
138
- }
139
- /**
140
- * Renders the block sorted by memory id (D6-10): confidence orders
141
- * selection, id orders output. Adding one candidate to a set of ten
142
- * must change exactly one line of the rendered block.
143
- */
144
- renderBlock(candidates) {
145
- const sorted = [...candidates].sort((a, b) => a.memoryId.localeCompare(b.memoryId));
146
- return `${sorted.map((c) => c.line).join("\n")}\n`;
147
- }
148
- /**
149
- * Plan §5.4 — strict dry run (D5-08): plan only, never write. Returns a
150
- * pending proposal per call, or [] when no candidate clears the floor.
151
- *
152
- * The proposal is a whole-block proposal: the persisted `proposed_text`
153
- * is the merged block (current block in the file + new candidate lines),
154
- * so approving it replaces the file block with exactly what was reviewed.
155
- * The current block is learned from `plan().before` — the Curator has no
156
- * fs capability, the writer reads (D6-01). Both plan calls are
157
- * deterministic, so a second propose() with unchanged inputs reproduces
158
- * the persisted diff byte-identically.
159
- *
160
- * A new proposal supersedes prior pending (or rejected) rows for the
161
- * same (project_id, kind, target_path) triple — §5.5's regeneration
162
- * arrow. Rows are never deleted (the audit trail is append-only).
163
- *
164
- * The schema's `memory_id` column is singular while a whole-block
165
- * proposal carries several ids; the contributing ids are stored joined
166
- * by "," (uuidv7 ids contain no comma) and split on read.
167
- */
168
- propose(kind, writer) {
169
- const targetPath = this.targetPathFor(kind);
170
- const candidates = this.candidates(undefined, this.sourceFromFlag());
171
- if (candidates.length === 0)
172
- return [];
173
- const readPlan = writer.plan(targetPath, this.renderBlock(candidates));
174
- const currentBlock = extractBlock(readPlan.before);
175
- // v0.7.0 (K7-013 / plan §4, D6-09) — de-duplicate against the WHOLE
176
- // file, not just the region between Kevin's markers. A convention the
177
- // user already wrote in their own words in their own section must not
178
- // be proposed back to them. The comparison is over the same normalized
179
- // tokens the fingerprint uses — no new similarity metric (D7-11).
180
- const entireFile = wholeFileLines(readPlan.before);
181
- const freshCandidates = candidates.filter((candidate) => !entireFile.has(normalizeBullet(candidate.line)));
182
- if (freshCandidates.length === 0 && currentBlock === "")
183
- return [];
184
- const newBlock = this.renderBlock(freshCandidates);
185
- const newLines = newBlock.split("\n").filter((l) => l !== "");
186
- const mergedBlock = newLines.length === 0
187
- ? currentBlock
188
- : currentBlock === ""
189
- ? `${newLines.join("\n")}\n`
190
- : `${currentBlock}${currentBlock.endsWith("\n") ? "" : "\n"}${newLines.join("\n")}\n`;
191
- // v0.7.0 (K7-013) — nothing new to propose: every candidate is already
192
- // in the file (inside or outside the markers). A vacuous proposal would
193
- // be noise; return none.
194
- const plan = writer.plan(targetPath, mergedBlock);
195
- const prior = this.store
196
- .prepare(`SELECT id FROM curation_proposals
197
- WHERE project_id = ? AND kind = ? AND target_path = ?
198
- AND status IN ('pending', 'rejected')`)
199
- .all(this.projectId, kind, targetPath);
200
- for (const row of prior) {
201
- this.transition(row.id, "supersede");
202
- }
203
- const memoryIds = freshCandidates.map((c) => c.memoryId);
204
- const id = uuidv7();
205
- this.store
206
- .prepare(`INSERT INTO curation_proposals
207
- (id, project_id, memory_id, kind, target_path,
208
- proposed_text, diff, status)
209
- VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')`)
210
- .run(id, this.projectId, memoryIds.join(","), kind, targetPath, mergedBlock, plan.diff);
211
- this.metrics?.incr("proposals_created", 1);
212
- const row = this.store
213
- .prepare("SELECT created_at FROM curation_proposals WHERE id = ?")
214
- .get(id);
215
- return [
216
- {
217
- id,
218
- kind,
219
- targetPath,
220
- memoryIds,
221
- proposedText: mergedBlock,
222
- diff: plan.diff,
223
- status: "pending",
224
- createdAt: row.created_at,
225
- },
226
- ];
227
- }
228
- /**
229
- * Plan §5.5 state machine, as explicit transitions with an exhaustive
230
- * switch — an unknown transition or an illegal source state throws
231
- * rather than silently no-oping. `decided_at` stamps approve/reject/
232
- * supersede, `applied_at` stamps apply.
233
- */
234
- transition(proposalId, transition) {
235
- const row = this.store
236
- .prepare("SELECT status FROM curation_proposals WHERE id = ?")
237
- .get(proposalId);
238
- if (!row) {
239
- throw new Error(`proposal not found: ${proposalId}`);
240
- }
241
- const status = row.status;
242
- let next = null;
243
- switch (transition) {
244
- case "approve":
245
- if (status === "pending")
246
- next = "approved";
247
- break;
248
- case "reject":
249
- if (status === "pending")
250
- next = "rejected";
251
- break;
252
- case "apply":
253
- if (status === "approved")
254
- next = "applied";
255
- break;
256
- case "supersede":
257
- if (status === "pending" || status === "rejected") {
258
- next = "superseded";
259
- }
260
- break;
261
- default:
262
- throw new Error(`unknown transition: ${transition}`);
263
- }
264
- if (next === null) {
265
- throw new Error(`illegal transition: ${status} -> ${transition}`);
266
- }
267
- if (next === "applied") {
268
- this.store
269
- .prepare("UPDATE curation_proposals SET status = 'applied', applied_at = datetime('now') WHERE id = ?")
270
- .run(proposalId);
271
- }
272
- else {
273
- this.store
274
- .prepare("UPDATE curation_proposals SET status = ?, decided_at = datetime('now') WHERE id = ?")
275
- .run(next, proposalId);
276
- }
277
- return next;
278
- }
279
- /** Plan §5.6 target paths. agents_md is a setting; skill/reference live under ~/.opencode-kevin. */
280
- targetPathFor(kind) {
281
- switch (kind) {
282
- case "agents_md":
283
- return this.memoryService.getSetting("agents_md_path", "AGENTS.md");
284
- case "skill":
285
- return join(homedir(), ".opencode-kevin", "skills", "project-knowledge.md");
286
- case "reference":
287
- return join(homedir(), ".opencode-kevin", "refs", "project-knowledge.md");
288
- }
289
- }
290
- }
291
- /** The block content currently between the markers, "" when absent or malformed. */
292
- function extractBlock(before) {
293
- const begin = before.indexOf(MARKER_BEGIN);
294
- const end = before.indexOf(MARKER_END);
295
- if (begin === -1 || end === -1 || end < begin)
296
- return "";
297
- return before
298
- .slice(begin + MARKER_BEGIN.length, end)
299
- .replace(/^\r?\n/, "")
300
- .replace(/\r?\n$/, "");
301
- }
302
- /**
303
- * v0.7.0 (K7-013 / plan §4) — the normalized set of bullet statements across
304
- * the WHOLE file, including any the user wrote outside Kevin's markers. Only
305
- * markdown bullets (`- ...`) are considered: prose and the marker lines are
306
- * never a candidate to propose back.
307
- */
308
- function wholeFileLines(before) {
309
- const set = new Set();
310
- for (const line of before.split(/\r?\n/)) {
311
- if (line.trim().startsWith("-")) {
312
- set.add(normalizeBullet(line));
313
- }
314
- }
315
- return set;
316
- }
317
- /** Normalize a rendered bullet for de-duplication — strip the marker prefix. */
318
- function normalizeBullet(line) {
319
- const statement = line
320
- .replace(/^\s*-\s+/, "")
321
- .trim()
322
- .replace(/\s+\((?:verified|feedback)[^)]*\)\s*$/i, "");
323
- return normalize(statement);
324
- }
@@ -1,5 +0,0 @@
1
- import type { TuiSnapshotSet } from "./tui-types.js";
2
- export declare function escapeHtml(text: string): string;
3
- export declare function proposalToken(proposalId: string, proposedText: string): string;
4
- export declare function renderDashboard(views: TuiSnapshotSet): string;
5
- export declare function writeDashboard(root: string, views: TuiSnapshotSet): string;