@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,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
|
-
}
|
package/dist/plugin/Curator.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/plugin/Curator.js
DELETED
|
@@ -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;
|