@jigyasudham/veto 2.9.0 → 3.1.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/README.md +83 -4
- package/dist/cli/statusline.d.ts +9 -0
- package/dist/cli/statusline.d.ts.map +1 -1
- package/dist/cli/statusline.js +59 -3
- package/dist/cli/statusline.js.map +1 -1
- package/dist/cli.js +166 -5
- package/dist/cli.js.map +1 -1
- package/dist/memory/config.d.ts +10 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +48 -8
- package/dist/memory/config.js.map +1 -1
- package/dist/memory/local.d.ts.map +1 -1
- package/dist/memory/local.js +32 -10
- package/dist/memory/local.js.map +1 -1
- package/dist/server/handlers/core.d.ts.map +1 -1
- package/dist/server/handlers/core.js +14 -1
- package/dist/server/handlers/core.js.map +1 -1
- package/dist/server/handlers/session.d.ts.map +1 -1
- package/dist/server/handlers/session.js +51 -2
- package/dist/server/handlers/session.js.map +1 -1
- package/dist/tools/definitions.d.ts +23 -3
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +11 -3
- package/dist/tools/definitions.js.map +1 -1
- package/dist/transcripts/adapters/claude.d.ts +27 -0
- package/dist/transcripts/adapters/claude.d.ts.map +1 -0
- package/dist/transcripts/adapters/claude.js +194 -0
- package/dist/transcripts/adapters/claude.js.map +1 -0
- package/dist/transcripts/archive.d.ts +23 -0
- package/dist/transcripts/archive.d.ts.map +1 -0
- package/dist/transcripts/archive.js +101 -0
- package/dist/transcripts/archive.js.map +1 -0
- package/dist/transcripts/cache.d.ts +66 -0
- package/dist/transcripts/cache.d.ts.map +1 -0
- package/dist/transcripts/cache.js +0 -0
- package/dist/transcripts/cache.js.map +1 -0
- package/dist/transcripts/chunk.d.ts +26 -0
- package/dist/transcripts/chunk.d.ts.map +1 -0
- package/dist/transcripts/chunk.js +54 -0
- package/dist/transcripts/chunk.js.map +1 -0
- package/dist/transcripts/config.d.ts +52 -0
- package/dist/transcripts/config.d.ts.map +1 -0
- package/dist/transcripts/config.js +135 -0
- package/dist/transcripts/config.js.map +1 -0
- package/dist/transcripts/embed.d.ts +58 -0
- package/dist/transcripts/embed.d.ts.map +1 -0
- package/dist/transcripts/embed.js +303 -0
- package/dist/transcripts/embed.js.map +1 -0
- package/dist/transcripts/expand.d.ts +20 -0
- package/dist/transcripts/expand.d.ts.map +1 -0
- package/dist/transcripts/expand.js +97 -0
- package/dist/transcripts/expand.js.map +1 -0
- package/dist/transcripts/ingest.d.ts +17 -0
- package/dist/transcripts/ingest.d.ts.map +1 -0
- package/dist/transcripts/ingest.js +120 -0
- package/dist/transcripts/ingest.js.map +1 -0
- package/dist/transcripts/manage.d.ts +42 -0
- package/dist/transcripts/manage.d.ts.map +1 -0
- package/dist/transcripts/manage.js +123 -0
- package/dist/transcripts/manage.js.map +1 -0
- package/dist/transcripts/mapping.d.ts +23 -0
- package/dist/transcripts/mapping.d.ts.map +1 -0
- package/dist/transcripts/mapping.js +42 -0
- package/dist/transcripts/mapping.js.map +1 -0
- package/dist/transcripts/mask.d.ts +26 -0
- package/dist/transcripts/mask.d.ts.map +1 -0
- package/dist/transcripts/mask.js +121 -0
- package/dist/transcripts/mask.js.map +1 -0
- package/dist/transcripts/on-save.d.ts +12 -0
- package/dist/transcripts/on-save.d.ts.map +1 -0
- package/dist/transcripts/on-save.js +37 -0
- package/dist/transcripts/on-save.js.map +1 -0
- package/dist/transcripts/pyramid.d.ts +38 -0
- package/dist/transcripts/pyramid.d.ts.map +1 -0
- package/dist/transcripts/pyramid.js +113 -0
- package/dist/transcripts/pyramid.js.map +1 -0
- package/dist/transcripts/recall.d.ts +46 -0
- package/dist/transcripts/recall.d.ts.map +1 -0
- package/dist/transcripts/recall.js +107 -0
- package/dist/transcripts/recall.js.map +1 -0
- package/dist/transcripts/schema.d.ts +79 -0
- package/dist/transcripts/schema.d.ts.map +1 -0
- package/dist/transcripts/schema.js +178 -0
- package/dist/transcripts/schema.js.map +1 -0
- package/dist/transcripts/search.d.ts +57 -0
- package/dist/transcripts/search.d.ts.map +1 -0
- package/dist/transcripts/search.js +332 -0
- package/dist/transcripts/search.js.map +1 -0
- package/dist/transcripts/store.d.ts +21 -0
- package/dist/transcripts/store.d.ts.map +1 -0
- package/dist/transcripts/store.js +105 -0
- package/dist/transcripts/store.js.map +1 -0
- package/dist/transcripts/toc.d.ts +18 -0
- package/dist/transcripts/toc.d.ts.map +1 -0
- package/dist/transcripts/toc.js +96 -0
- package/dist/transcripts/toc.js.map +1 -0
- package/dist/transcripts/tokenize.d.ts +14 -0
- package/dist/transcripts/tokenize.d.ts.map +1 -0
- package/dist/transcripts/tokenize.js +70 -0
- package/dist/transcripts/tokenize.js.map +1 -0
- package/dist/transcripts/vectors.d.ts +40 -0
- package/dist/transcripts/vectors.d.ts.map +1 -0
- package/dist/transcripts/vectors.js +203 -0
- package/dist/transcripts/vectors.js.map +1 -0
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Portable BM25 search over indexed events (VERSION-3 item 6, Step 10 — redone).
|
|
2
|
+
//
|
|
3
|
+
// Plain-SQLite inverted index (search_docs / search_terms / search_postings),
|
|
4
|
+
// scored in JS. Core SQL only — no FTS5, no math functions, nothing behind a
|
|
5
|
+
// compile-time flag; that is the lesson of PR #28 (`no such module: fts5`).
|
|
6
|
+
// Query terms are bound parameters resolved against the term dictionary, so
|
|
7
|
+
// there is no query language and no injection surface at all.
|
|
8
|
+
//
|
|
9
|
+
// Contract change vs the FTS5 version: score is positive, HIGHER is better
|
|
10
|
+
// (FTS5's bm25() was negative-lower-is-better). Snippets are built in JS from
|
|
11
|
+
// events.text for the returned rows only.
|
|
12
|
+
import { getTranscriptsDb } from './store.js';
|
|
13
|
+
import { normalizeProjectDir } from '../memory/local.js';
|
|
14
|
+
import { tokenize } from './tokenize.js';
|
|
15
|
+
import { STRIDE_CHARS, WINDOW_CHARS } from './chunk.js';
|
|
16
|
+
import { searchVectors } from './vectors.js';
|
|
17
|
+
import { getDocCache, scopeMask } from './cache.js';
|
|
18
|
+
const K1 = 1.2;
|
|
19
|
+
const B = 0.75;
|
|
20
|
+
// Terms present in more than half the corpus carry ~no signal but force
|
|
21
|
+
// materializing huge posting lists (there is no LIMIT that is safe before
|
|
22
|
+
// ranking). Dropped at the dictionary, before any postings are read — unless
|
|
23
|
+
// every query term is that common, in which case they are all kept.
|
|
24
|
+
const DF_CAP_RATIO = 0.5;
|
|
25
|
+
const SNIPPET_TOKENS = 12;
|
|
26
|
+
/** BM25, scored in JS because log() in SQL is a compile-time lottery. */
|
|
27
|
+
function bm25(tf, df, n, len, avgdl) {
|
|
28
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
29
|
+
return idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (avgdl > 0 ? len / avgdl : 1)));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a highlighted excerpt in JS — the moral equivalent of FTS5's
|
|
33
|
+
* snippet(…, '[', ']', '…', 12): the SNIPPET_TOKENS-token window containing
|
|
34
|
+
* the most matched terms, matches wrapped in [ ], ellipses when clipped.
|
|
35
|
+
*/
|
|
36
|
+
export function buildSnippet(text, terms) {
|
|
37
|
+
const re = /[\p{L}\p{N}_\-./@+]+/gu;
|
|
38
|
+
const tokens = [];
|
|
39
|
+
for (const m of text.matchAll(re)) {
|
|
40
|
+
const raw = m[0];
|
|
41
|
+
const matched = tokenize(raw).some(t => terms.has(t));
|
|
42
|
+
tokens.push({ start: m.index, end: m.index + raw.length, matched });
|
|
43
|
+
}
|
|
44
|
+
if (tokens.length === 0)
|
|
45
|
+
return text.slice(0, 120);
|
|
46
|
+
// Best SNIPPET_TOKENS-token window = most matches, earliest wins ties.
|
|
47
|
+
let best = 0;
|
|
48
|
+
let bestCount = -1;
|
|
49
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
50
|
+
const windowEnd = Math.min(i + SNIPPET_TOKENS, tokens.length);
|
|
51
|
+
let count = 0;
|
|
52
|
+
for (let j = i; j < windowEnd; j += 1)
|
|
53
|
+
if (tokens[j].matched)
|
|
54
|
+
count += 1;
|
|
55
|
+
if (count > bestCount) {
|
|
56
|
+
bestCount = count;
|
|
57
|
+
best = i;
|
|
58
|
+
}
|
|
59
|
+
if (windowEnd === tokens.length)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const windowEnd = Math.min(best + SNIPPET_TOKENS, tokens.length);
|
|
63
|
+
let out = '';
|
|
64
|
+
let pos = tokens[best].start;
|
|
65
|
+
for (let j = best; j < windowEnd; j += 1) {
|
|
66
|
+
const t = tokens[j];
|
|
67
|
+
out += text.slice(pos, t.start);
|
|
68
|
+
out += t.matched ? `[${text.slice(t.start, t.end)}]` : text.slice(t.start, t.end);
|
|
69
|
+
pos = t.end;
|
|
70
|
+
}
|
|
71
|
+
const prefix = tokens[best].start > 0 ? '…' : '';
|
|
72
|
+
const suffix = windowEnd < tokens.length || tokens[windowEnd - 1].end < text.length ? '…' : '';
|
|
73
|
+
return (prefix + out.replace(/\s+/g, ' ').trim() + suffix);
|
|
74
|
+
}
|
|
75
|
+
export function searchEvents(query, opts = {}) {
|
|
76
|
+
const db = getTranscriptsDb();
|
|
77
|
+
const terms = [...new Set(tokenize(query))];
|
|
78
|
+
if (terms.length === 0)
|
|
79
|
+
return [];
|
|
80
|
+
const docs = getDocCache();
|
|
81
|
+
const stats = { n: docs.count, avgdl: docs.avgdl };
|
|
82
|
+
if (stats.n === 0)
|
|
83
|
+
return [];
|
|
84
|
+
// Resolve terms against the dictionary in one round trip.
|
|
85
|
+
const marks = terms.map(() => '?').join(', ');
|
|
86
|
+
const found = db.prepare(`SELECT id, term FROM search_terms WHERE term IN (${marks})`).all(...terms);
|
|
87
|
+
if (found.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
// df per term — an index-only range count on the postings PK.
|
|
90
|
+
const dfStmt = db.prepare(`SELECT COUNT(*) AS n FROM search_postings WHERE term_id = ?`);
|
|
91
|
+
const withDf = found.map(t => ({ ...t, df: dfStmt.get(t.id).n }));
|
|
92
|
+
let kept = withDf.filter(t => t.df / stats.n <= DF_CAP_RATIO);
|
|
93
|
+
if (kept.length === 0)
|
|
94
|
+
kept = withDf; // every term is that common — keep them all
|
|
95
|
+
// Postings only — three narrow columns, no join. Joining search_docs per
|
|
96
|
+
// POSTING to fetch columns needed for the final handful of rows cost 7.1 ms
|
|
97
|
+
// per common term at 50 sessions; this costs 2.6 ms. Scope and length come
|
|
98
|
+
// from the cached metadata instead, and full rows are read for the top-K.
|
|
99
|
+
const rows = db.prepare(`SELECT term_id, doc_id, tf FROM search_postings WHERE term_id IN (${kept.map(() => '?').join(', ')})`).all(...kept.map(t => t.id));
|
|
100
|
+
if (rows.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
const mask = scopeMask(docs, {
|
|
103
|
+
projectDir: opts.projectDir ? normalizeProjectDir(opts.projectDir) : undefined,
|
|
104
|
+
sourceSessionId: opts.sourceSessionId,
|
|
105
|
+
});
|
|
106
|
+
const dfById = new Map(kept.map(t => [t.id, t.df]));
|
|
107
|
+
const acc = new Map();
|
|
108
|
+
for (const r of rows) {
|
|
109
|
+
if (mask && mask[r.doc_id] !== 1)
|
|
110
|
+
continue;
|
|
111
|
+
const df = dfById.get(r.term_id) ?? 1;
|
|
112
|
+
const inc = bm25(r.tf, df, stats.n, docs.len[r.doc_id] ?? 0, stats.avgdl);
|
|
113
|
+
acc.set(r.doc_id, (acc.get(r.doc_id) ?? 0) + inc);
|
|
114
|
+
}
|
|
115
|
+
if (acc.size === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const limit = Math.min(Math.max(opts.limit ?? 10, 1), 50);
|
|
118
|
+
const top = [...acc.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
119
|
+
// Metadata and text for the returned rows only.
|
|
120
|
+
const docIds = top.map(([id]) => id);
|
|
121
|
+
const marks2 = docIds.map(() => '?').join(', ');
|
|
122
|
+
const metaRows = db.prepare(`SELECT d.id, d.event_id, d.archive_id, d.source_session_id, d.seq, d.kind, e.text
|
|
123
|
+
FROM search_docs d JOIN events e ON e.id = d.event_id
|
|
124
|
+
WHERE d.id IN (${marks2})`).all(...docIds);
|
|
125
|
+
const metaById = new Map(metaRows.map(r => [r.id, r]));
|
|
126
|
+
const termSet = new Set(kept.map(t => t.term));
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const [docId, score] of top) {
|
|
129
|
+
const m = metaById.get(docId);
|
|
130
|
+
if (!m)
|
|
131
|
+
continue;
|
|
132
|
+
out.push({
|
|
133
|
+
eventId: m.event_id,
|
|
134
|
+
archiveId: m.archive_id,
|
|
135
|
+
sourceSessionId: m.source_session_id,
|
|
136
|
+
seq: m.seq,
|
|
137
|
+
kind: m.kind,
|
|
138
|
+
snippet: buildSnippet(m.text ?? '', termSet),
|
|
139
|
+
score,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
// --- Hybrid retrieval -------------------------------------------------------
|
|
145
|
+
// How deep each ranker is consulted before fusing. Deeper than the returned
|
|
146
|
+
// limit so a result ranked mid-list by one ranker can still be lifted by the
|
|
147
|
+
// other — the entire mechanism by which a paraphrase beats keyword overlap.
|
|
148
|
+
const CANDIDATE_DEPTH = 50;
|
|
149
|
+
/**
|
|
150
|
+
* Reciprocal-rank fusion constant.
|
|
151
|
+
*
|
|
152
|
+
* NOT 60. That value comes from the RRF paper's TREC runs over ~1000-document
|
|
153
|
+
* lists; at our candidate depth it is actively harmful. With k=60 and d=50 the
|
|
154
|
+
* whole ranking compresses into a 1.8x spread (rank 1 = 0.0164, rank 50 =
|
|
155
|
+
* 0.0091), which destroys rank information and lets agreement dominate
|
|
156
|
+
* quality: two mid-list agreements (0.011 + 0.011) outscore one top-ranked
|
|
157
|
+
* single-ranker find (0.0164).
|
|
158
|
+
*
|
|
159
|
+
* That is fatal for the case this layer exists to serve. A strict paraphrase
|
|
160
|
+
* shares no vocabulary with its target, so the target is found by the semantic
|
|
161
|
+
* ranker ALONE and earns exactly one contribution — while any keyword-adjacent
|
|
162
|
+
* event earns a comparable one for free. The semantic discovery loses to noise.
|
|
163
|
+
*
|
|
164
|
+
* Requirement: a rank-1 hit from a single ranker must be able to outrank a pair
|
|
165
|
+
* of mid-list agreements. 1/(k+1) > 2/(k+d/2) solves to k < d/2 - 2, i.e.
|
|
166
|
+
* k < 23 at d=50. 10 sits comfortably inside that bound and keeps the top of
|
|
167
|
+
* the list sharply separated (rank 1 = 0.091, rank 10 = 0.050, rank 50 = 0.017).
|
|
168
|
+
*/
|
|
169
|
+
const RRF_K = 10;
|
|
170
|
+
/**
|
|
171
|
+
* BM25 contributes only candidates it is actually confident about: anything
|
|
172
|
+
* below this fraction of its own top score is tail noise that would otherwise
|
|
173
|
+
* occupy fusion slots and crowd out semantic finds.
|
|
174
|
+
*
|
|
175
|
+
* Applied to BM25 only, deliberately. The two rankers have different score
|
|
176
|
+
* geometries: BM25 decays fast and spans orders of magnitude, so relative
|
|
177
|
+
* score is meaningful. Cosines sit in a narrow band — measured on the real
|
|
178
|
+
* corpus, off-topic queries reach 0.40 while on-topic start at 0.45 — so a
|
|
179
|
+
* relative floor there would be noise itself. Depth is the semantic side's
|
|
180
|
+
* only honest control.
|
|
181
|
+
*
|
|
182
|
+
* Only the tail is affected, so a strong keyword hit can never be dropped and
|
|
183
|
+
* lexical survival is preserved by construction.
|
|
184
|
+
*/
|
|
185
|
+
const BM25_RELATIVE_FLOOR = 0.25;
|
|
186
|
+
/**
|
|
187
|
+
* One ranker's contribution for a 0-based rank.
|
|
188
|
+
*
|
|
189
|
+
* Exported so the property the fix turns on can be asserted directly rather
|
|
190
|
+
* than through retrieval, where it is not reliably constructible: with a
|
|
191
|
+
* bag-of-words embedding model, lexical overlap implies semantic similarity,
|
|
192
|
+
* so a document that matches a query's words but not its meaning barely
|
|
193
|
+
* exists. The arithmetic, however, is exact.
|
|
194
|
+
*/
|
|
195
|
+
export function rrfWeight(rank0) {
|
|
196
|
+
return 1 / (RRF_K + rank0 + 1);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Fuse ranked id lists by reciprocal rank. Pure, for testing and reuse.
|
|
200
|
+
*
|
|
201
|
+
* The property that matters: a top-ranked find from ONE ranker must be able
|
|
202
|
+
* to outrank a document both rankers place mid-list. That is the case the
|
|
203
|
+
* semantic layer exists to serve — a strict paraphrase is found by the
|
|
204
|
+
* semantic ranker alone — and it is exactly what k=60 at depth 50 broke.
|
|
205
|
+
*/
|
|
206
|
+
export function fuseByRank(lists) {
|
|
207
|
+
const acc = new Map();
|
|
208
|
+
for (const list of lists) {
|
|
209
|
+
list.forEach((id, i) => acc.set(id, (acc.get(id) ?? 0) + rrfWeight(i)));
|
|
210
|
+
}
|
|
211
|
+
return [...acc.entries()]
|
|
212
|
+
.map(([id, score]) => ({ id, score }))
|
|
213
|
+
.sort((a, b) => b.score - a.score);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* BM25 and semantic recall, fused by reciprocal rank.
|
|
217
|
+
*
|
|
218
|
+
* RRF over raw score blending is deliberate: BM25 scores are unbounded and
|
|
219
|
+
* corpus-dependent while cosines sit in [-1, 1], so any weighted sum would be
|
|
220
|
+
* a hidden tuning knob that drifts with corpus size. Ranks have neither
|
|
221
|
+
* problem, and lexical survival is measurable (a keyword hit must not be
|
|
222
|
+
* pushed out of the fused list by semantic noise).
|
|
223
|
+
*
|
|
224
|
+
* Degrades silently to BM25 alone when no vectors are stored or the model is
|
|
225
|
+
* not installed (A2) — the caller cannot tell the difference except by score.
|
|
226
|
+
*/
|
|
227
|
+
export function searchEventsHybrid(query, opts = {}) {
|
|
228
|
+
const limit = Math.min(Math.max(opts.limit ?? 10, 1), 50);
|
|
229
|
+
const lexicalAll = searchEvents(query, { ...opts, limit: CANDIDATE_DEPTH });
|
|
230
|
+
const lexFloor = (lexicalAll[0]?.score ?? 0) * BM25_RELATIVE_FLOOR;
|
|
231
|
+
const lexical = lexicalAll.filter(h => h.score >= lexFloor);
|
|
232
|
+
// Cosine ranking has no notion of "no match" — it always returns its nearest
|
|
233
|
+
// neighbours, however far away they are. A similarity floor cannot fix that
|
|
234
|
+
// here: measured on the real corpus, off-topic queries reach 0.40 while true
|
|
235
|
+
// strict paraphrases go as low as 0.21, so the distributions overlap and any
|
|
236
|
+
// threshold that suppresses noise also cuts the recall this layer exists for.
|
|
237
|
+
//
|
|
238
|
+
// What DOES separate them is whether the query has any footing in the corpus
|
|
239
|
+
// at all. A paraphrase shares no words with its target but still speaks the
|
|
240
|
+
// corpus's language; `zzz-nonexistent-term` shares nothing with anything. So
|
|
241
|
+
// the semantic layer is allowed to generalize, never to extrapolate from a
|
|
242
|
+
// query the corpus has no word of — and "nothing matched" stays expressible.
|
|
243
|
+
// Unfiltered on the fallback paths: the relative floor exists to stop tail
|
|
244
|
+
// noise occupying FUSION slots. With no fusion happening it would just be
|
|
245
|
+
// hiding results the lexical ranker legitimately found.
|
|
246
|
+
if (!queryHasCorpusTerms(query))
|
|
247
|
+
return lexicalAll.slice(0, limit);
|
|
248
|
+
let semantic = [];
|
|
249
|
+
try {
|
|
250
|
+
semantic = searchVectors(query, {
|
|
251
|
+
projectDir: opts.projectDir ? normalizeProjectDir(opts.projectDir) : undefined,
|
|
252
|
+
sourceSessionId: opts.sourceSessionId,
|
|
253
|
+
limit: CANDIDATE_DEPTH,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
semantic = []; // a broken model must not take the lexical path down with it
|
|
258
|
+
}
|
|
259
|
+
if (semantic.length === 0)
|
|
260
|
+
return lexicalAll.slice(0, limit);
|
|
261
|
+
const db = getTranscriptsDb();
|
|
262
|
+
// Resolve the semantic hits' doc ids to event metadata in one round trip.
|
|
263
|
+
const docIds = semantic.map(s => s.docId);
|
|
264
|
+
const docRows = db.prepare(`SELECT id, event_id, archive_id, source_session_id, seq, kind
|
|
265
|
+
FROM search_docs WHERE id IN (${docIds.map(() => '?').join(', ')})`).all(...docIds);
|
|
266
|
+
const docById = new Map(docRows.map(r => [r.id, r]));
|
|
267
|
+
const fused = new Map();
|
|
268
|
+
lexical.forEach((hit, i) => {
|
|
269
|
+
fused.set(hit.eventId, { hit, rrf: rrfWeight(i) });
|
|
270
|
+
});
|
|
271
|
+
semantic.forEach((s, i) => {
|
|
272
|
+
const doc = docById.get(s.docId);
|
|
273
|
+
if (!doc)
|
|
274
|
+
return;
|
|
275
|
+
const prev = fused.get(doc.event_id);
|
|
276
|
+
const inc = rrfWeight(i);
|
|
277
|
+
if (prev) {
|
|
278
|
+
prev.rrf += inc;
|
|
279
|
+
prev.chunkIndex = s.chunkIndex;
|
|
280
|
+
}
|
|
281
|
+
else
|
|
282
|
+
fused.set(doc.event_id, { doc, chunkIndex: s.chunkIndex, rrf: inc });
|
|
283
|
+
});
|
|
284
|
+
const ranked = [...fused.entries()]
|
|
285
|
+
.sort((a, b) => b[1].rrf - a[1].rrf)
|
|
286
|
+
.slice(0, limit);
|
|
287
|
+
// Text is needed only for entries that arrived semantically — lexical hits
|
|
288
|
+
// already carry a snippet built from their matched terms.
|
|
289
|
+
const needText = ranked.filter(([, e]) => !e.hit).map(([eventId]) => eventId);
|
|
290
|
+
const textById = new Map();
|
|
291
|
+
if (needText.length > 0) {
|
|
292
|
+
const rows = db.prepare(`SELECT id, text FROM events WHERE id IN (${needText.map(() => '?').join(', ')})`).all(...needText);
|
|
293
|
+
for (const r of rows)
|
|
294
|
+
textById.set(r.id, r.text ?? '');
|
|
295
|
+
}
|
|
296
|
+
const terms = new Set(tokenize(query));
|
|
297
|
+
return ranked.map(([eventId, e]) => {
|
|
298
|
+
if (e.hit)
|
|
299
|
+
return { ...e.hit, score: e.rrf };
|
|
300
|
+
const doc = e.doc;
|
|
301
|
+
const text = textById.get(eventId) ?? '';
|
|
302
|
+
return {
|
|
303
|
+
eventId,
|
|
304
|
+
archiveId: doc.archive_id,
|
|
305
|
+
sourceSessionId: doc.source_session_id,
|
|
306
|
+
seq: doc.seq,
|
|
307
|
+
kind: doc.kind,
|
|
308
|
+
// Aim the excerpt at the window that actually matched, so a long event
|
|
309
|
+
// does not open on an unrelated first paragraph (C6's opportunity).
|
|
310
|
+
snippet: buildSnippet(windowAround(text, e.chunkIndex ?? 0), terms),
|
|
311
|
+
score: e.rrf,
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** True when at least one query token appears in the corpus dictionary. */
|
|
316
|
+
function queryHasCorpusTerms(query) {
|
|
317
|
+
const terms = [...new Set(tokenize(query))];
|
|
318
|
+
if (terms.length === 0)
|
|
319
|
+
return false;
|
|
320
|
+
const db = getTranscriptsDb();
|
|
321
|
+
const hit = db.prepare(`SELECT 1 AS present FROM search_terms WHERE term IN (${terms.map(() => '?').join(', ')}) LIMIT 1`).get(...terms);
|
|
322
|
+
return hit !== undefined;
|
|
323
|
+
}
|
|
324
|
+
/** The slice of `text` the winning chunk covered, widened by one stride each
|
|
325
|
+
* way so the snippet has context to work with. */
|
|
326
|
+
function windowAround(text, chunkIndex) {
|
|
327
|
+
if (text.length <= WINDOW_CHARS)
|
|
328
|
+
return text;
|
|
329
|
+
const start = Math.max(0, chunkIndex * STRIDE_CHARS - STRIDE_CHARS);
|
|
330
|
+
return text.slice(start, start + WINDOW_CHARS + STRIDE_CHARS * 2);
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/transcripts/search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { TRANSCRIPTS_SCHEMA_VERSION } from './schema.js';
|
|
3
|
+
/** True when node:sqlite can be loaded — capture is silently unavailable otherwise. */
|
|
4
|
+
export declare function transcriptsAvailable(): boolean;
|
|
5
|
+
/** Root dir for all transcript data (archives + sidecar DB). Env override for tests. */
|
|
6
|
+
export declare function transcriptsRoot(): string;
|
|
7
|
+
/** Path to the sidecar index DB. Env override for tests (mirrors VETO_TEST_DB). */
|
|
8
|
+
export declare function transcriptsDbPath(): string;
|
|
9
|
+
/** Dir holding the L0 .gz archive files (populated in Step 5). */
|
|
10
|
+
export declare function archiveDir(): string;
|
|
11
|
+
export declare function resetTranscriptsDb(): void;
|
|
12
|
+
export declare function getTranscriptsDb(): DatabaseSync;
|
|
13
|
+
/**
|
|
14
|
+
* Apply every migration newer than the DB's current user_version, each in its
|
|
15
|
+
* own transaction. Idempotent: re-running is a no-op once caught up. Returns the
|
|
16
|
+
* resulting schema version.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runMigrations(db: DatabaseSync): number;
|
|
19
|
+
export declare function schemaVersion(db?: DatabaseSync): number;
|
|
20
|
+
export { TRANSCRIPTS_SCHEMA_VERSION };
|
|
21
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/transcripts/store.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,OAAO,EAAc,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAYrE,uFAAuF;AACvF,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED,wFAAwF;AACxF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,kEAAkE;AAClE,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAKD,wBAAgB,kBAAkB,IAAI,IAAI,CAMzC;AAED,wBAAgB,gBAAgB,IAAI,YAAY,CAc/C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM,CAiBtD;AAOD,wBAAgB,aAAa,CAAC,EAAE,GAAE,YAAiC,GAAG,MAAM,CAE3E;AAED,OAAO,EAAE,0BAA0B,EAAE,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Sidecar transcripts.db connection + additive migration runner
|
|
2
|
+
// (VERSION-3 item 6, Step 2).
|
|
3
|
+
//
|
|
4
|
+
// Opened lazily (node:sqlite is a Node 22.5+ built-in; loaded via createRequire
|
|
5
|
+
// so bundlers skip it and older runtimes degrade gracefully). WAL + busy_timeout
|
|
6
|
+
// let the HUD keep reading veto.db while capture writes here without lock
|
|
7
|
+
// contention. This module NEVER opens veto.db — the two DBs stay isolated.
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { mkdirSync } from 'node:fs';
|
|
11
|
+
import { effectiveTranscriptsDir } from './config.js';
|
|
12
|
+
import { MIGRATIONS, TRANSCRIPTS_SCHEMA_VERSION } from './schema.js';
|
|
13
|
+
const _require = createRequire(import.meta.url);
|
|
14
|
+
let _DbSync = null;
|
|
15
|
+
function requireDbSync() {
|
|
16
|
+
if (!_DbSync) {
|
|
17
|
+
_DbSync = _require('node:sqlite').DatabaseSync;
|
|
18
|
+
}
|
|
19
|
+
return _DbSync;
|
|
20
|
+
}
|
|
21
|
+
/** True when node:sqlite can be loaded — capture is silently unavailable otherwise. */
|
|
22
|
+
export function transcriptsAvailable() {
|
|
23
|
+
try {
|
|
24
|
+
requireDbSync();
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Root dir for all transcript data (archives + sidecar DB). Env override for tests. */
|
|
32
|
+
export function transcriptsRoot() {
|
|
33
|
+
return process.env.VETO_TRANSCRIPTS_DIR ?? effectiveTranscriptsDir();
|
|
34
|
+
}
|
|
35
|
+
/** Path to the sidecar index DB. Env override for tests (mirrors VETO_TEST_DB). */
|
|
36
|
+
export function transcriptsDbPath() {
|
|
37
|
+
return process.env.VETO_TRANSCRIPTS_DB ?? join(transcriptsRoot(), 'index.db');
|
|
38
|
+
}
|
|
39
|
+
/** Dir holding the L0 .gz archive files (populated in Step 5). */
|
|
40
|
+
export function archiveDir() {
|
|
41
|
+
return join(transcriptsRoot(), 'archives');
|
|
42
|
+
}
|
|
43
|
+
let _db = null;
|
|
44
|
+
let _openedPath = null;
|
|
45
|
+
export function resetTranscriptsDb() {
|
|
46
|
+
if (_db) {
|
|
47
|
+
try {
|
|
48
|
+
_db.close();
|
|
49
|
+
}
|
|
50
|
+
catch { /* ignore */ }
|
|
51
|
+
_db = null;
|
|
52
|
+
_openedPath = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function getTranscriptsDb() {
|
|
56
|
+
const path = transcriptsDbPath();
|
|
57
|
+
if (_db && _openedPath === path)
|
|
58
|
+
return _db;
|
|
59
|
+
if (_db)
|
|
60
|
+
resetTranscriptsDb();
|
|
61
|
+
const DbSync = requireDbSync();
|
|
62
|
+
if (path !== ':memory:')
|
|
63
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
64
|
+
const db = new DbSync(path);
|
|
65
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
66
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
67
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
68
|
+
runMigrations(db);
|
|
69
|
+
_db = db;
|
|
70
|
+
_openedPath = path;
|
|
71
|
+
return db;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Apply every migration newer than the DB's current user_version, each in its
|
|
75
|
+
* own transaction. Idempotent: re-running is a no-op once caught up. Returns the
|
|
76
|
+
* resulting schema version.
|
|
77
|
+
*/
|
|
78
|
+
export function runMigrations(db) {
|
|
79
|
+
let applied = readUserVersion(db);
|
|
80
|
+
for (const m of MIGRATIONS) {
|
|
81
|
+
if (m.version > applied) {
|
|
82
|
+
db.exec('BEGIN');
|
|
83
|
+
try {
|
|
84
|
+
db.exec(m.up);
|
|
85
|
+
db.exec(`PRAGMA user_version = ${m.version}`);
|
|
86
|
+
db.exec('COMMIT');
|
|
87
|
+
applied = m.version;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
db.exec('ROLLBACK');
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return applied;
|
|
96
|
+
}
|
|
97
|
+
function readUserVersion(db) {
|
|
98
|
+
const row = db.prepare('PRAGMA user_version').get();
|
|
99
|
+
return row?.user_version ?? 0;
|
|
100
|
+
}
|
|
101
|
+
export function schemaVersion(db = getTranscriptsDb()) {
|
|
102
|
+
return readUserVersion(db);
|
|
103
|
+
}
|
|
104
|
+
export { TRANSCRIPTS_SCHEMA_VERSION };
|
|
105
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/transcripts/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Segment = {
|
|
2
|
+
index: number;
|
|
3
|
+
fromSeq: number;
|
|
4
|
+
toSeq: number;
|
|
5
|
+
firstTs: string | null;
|
|
6
|
+
lastTs: string | null;
|
|
7
|
+
title: string;
|
|
8
|
+
userMessages: number;
|
|
9
|
+
assistantMessages: number;
|
|
10
|
+
toolCalls: number;
|
|
11
|
+
files: string[];
|
|
12
|
+
tools: string[];
|
|
13
|
+
hasError: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function buildTOC(archiveId: string): Segment[];
|
|
16
|
+
/** Compact one-line-per-segment rendering for the recall tool's first response. */
|
|
17
|
+
export declare function renderTOC(segments: Segment[]): string;
|
|
18
|
+
//# sourceMappingURL=toc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toc.d.ts","sourceRoot":"","sources":["../../src/transcripts/toc.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AASF,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,EAAE,CAkDrD;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAQrD"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Metadata table-of-contents / phase segmentation (VERSION-3 item 6, Step 9).
|
|
2
|
+
//
|
|
3
|
+
// B1 of the vectorless-retrieval pipeline: split a session into phases so the
|
|
4
|
+
// recall tool can hand the host AI a compact map to navigate BEFORE any text
|
|
5
|
+
// search. Boundaries are deterministic — a new segment begins at each real user
|
|
6
|
+
// message (a user prompt + the assistant's response + tool activity until the
|
|
7
|
+
// next prompt). Events before the first user prompt form a preamble segment.
|
|
8
|
+
// Computed on demand from the events table (no extra storage).
|
|
9
|
+
import { getTranscriptsDb } from './store.js';
|
|
10
|
+
import { EDIT_TOOLS, FILE_RE, ERROR_RE } from './pyramid.js';
|
|
11
|
+
const TITLE_MAX = 80;
|
|
12
|
+
function preview(text) {
|
|
13
|
+
const t = text.replace(/\s+/g, ' ').trim();
|
|
14
|
+
return t.length > TITLE_MAX ? t.slice(0, TITLE_MAX - 1) + '…' : t;
|
|
15
|
+
}
|
|
16
|
+
export function buildTOC(archiveId) {
|
|
17
|
+
const db = getTranscriptsDb();
|
|
18
|
+
const rows = db.prepare(`SELECT seq, kind, tool_name, text, ts_utc FROM events WHERE archive_id = ? ORDER BY seq`).all(archiveId);
|
|
19
|
+
if (rows.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
const segments = [];
|
|
22
|
+
let cur = null;
|
|
23
|
+
const open = (first, title) => {
|
|
24
|
+
cur = {
|
|
25
|
+
index: segments.length, fromSeq: first.seq, toSeq: first.seq,
|
|
26
|
+
firstTs: first.ts_utc, lastTs: first.ts_utc, title,
|
|
27
|
+
userMessages: 0, assistantMessages: 0, toolCalls: 0, files: [], tools: [], hasError: false,
|
|
28
|
+
_files: new Set(), _tools: new Set(),
|
|
29
|
+
};
|
|
30
|
+
segments.push(cur);
|
|
31
|
+
};
|
|
32
|
+
const close = () => {
|
|
33
|
+
if (cur) {
|
|
34
|
+
cur.files = [...cur._files];
|
|
35
|
+
cur.tools = [...cur._tools];
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
for (const r of rows) {
|
|
39
|
+
if (r.kind === 'user_message') {
|
|
40
|
+
close();
|
|
41
|
+
open(r, preview(r.text ?? '') || `segment ${segments.length + 1}`);
|
|
42
|
+
}
|
|
43
|
+
else if (!cur) {
|
|
44
|
+
// Preamble: events before the first user message.
|
|
45
|
+
open(r, '(session start)');
|
|
46
|
+
}
|
|
47
|
+
const c = cur;
|
|
48
|
+
c.toSeq = r.seq;
|
|
49
|
+
if (r.ts_utc) {
|
|
50
|
+
if (!c.firstTs || r.ts_utc < c.firstTs)
|
|
51
|
+
c.firstTs = r.ts_utc;
|
|
52
|
+
if (!c.lastTs || r.ts_utc > c.lastTs)
|
|
53
|
+
c.lastTs = r.ts_utc;
|
|
54
|
+
}
|
|
55
|
+
switch (r.kind) {
|
|
56
|
+
case 'user_message':
|
|
57
|
+
c.userMessages++;
|
|
58
|
+
break;
|
|
59
|
+
case 'assistant_message':
|
|
60
|
+
c.assistantMessages++;
|
|
61
|
+
break;
|
|
62
|
+
case 'tool_call': {
|
|
63
|
+
c.toolCalls++;
|
|
64
|
+
const name = r.tool_name ?? 'tool';
|
|
65
|
+
c._tools.add(name);
|
|
66
|
+
if (r.text && EDIT_TOOLS.has(name)) {
|
|
67
|
+
const m = r.text.match(FILE_RE);
|
|
68
|
+
if (m)
|
|
69
|
+
c._files.add(m[1]);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'tool_result':
|
|
74
|
+
if (r.text && ERROR_RE.test(r.text))
|
|
75
|
+
c.hasError = true;
|
|
76
|
+
break;
|
|
77
|
+
default: break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
close();
|
|
81
|
+
return segments;
|
|
82
|
+
}
|
|
83
|
+
/** Compact one-line-per-segment rendering for the recall tool's first response. */
|
|
84
|
+
export function renderTOC(segments) {
|
|
85
|
+
return segments.map(s => {
|
|
86
|
+
const bits = [`#${s.index} [${s.fromSeq}-${s.toSeq}]`];
|
|
87
|
+
if (s.hasError)
|
|
88
|
+
bits.push('⚠err');
|
|
89
|
+
if (s.files.length)
|
|
90
|
+
bits.push(`files:${s.files.slice(0, 4).join(',')}`);
|
|
91
|
+
if (s.tools.length)
|
|
92
|
+
bits.push(`tools:${s.tools.slice(0, 4).join(',')}`);
|
|
93
|
+
return `${bits.join(' ')} ${s.title}`;
|
|
94
|
+
}).join('\n');
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=toc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toc.js","sourceRoot":"","sources":["../../src/transcripts/toc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Sub-token expansion on by default; the ingest benchmark measures both ways. */
|
|
2
|
+
export declare const SUBTOKENS_DEFAULT = true;
|
|
3
|
+
/**
|
|
4
|
+
* Tokenize text for indexing or querying — always the same function.
|
|
5
|
+
*
|
|
6
|
+
* 1. Split on anything that is not a letter, digit, or `_ - . / @ +`.
|
|
7
|
+
* 2. Emit each token lowercased.
|
|
8
|
+
* 3. When it looks compound, also emit its lowercased sub-tokens:
|
|
9
|
+
* `normalizeProjectDir` → normalizeprojectdir, normalize, project, dir;
|
|
10
|
+
* `src/transcripts/search.ts` → the full path, src, transcripts, search, ts.
|
|
11
|
+
* 4. Drop blobs (see isBlob) and tokens with no letter or digit at all.
|
|
12
|
+
*/
|
|
13
|
+
export declare function tokenize(text: string, subtokens?: boolean): string[];
|
|
14
|
+
//# sourceMappingURL=tokenize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/transcripts/tokenize.ts"],"names":[],"mappings":"AAgCA,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,OAA2B,GAAG,MAAM,EAAE,CAsBvF"}
|