sensemaking 0.24.3 → 0.24.5
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 +16 -12
- package/dist/cjs/cli/status.js +14 -24
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/commands/search.js +22 -0
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/errors.d.cts +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/lib/worker-file.d.cts +1 -0
- package/dist/cjs/lib/worker-file.d.ts +1 -0
- package/dist/cjs/lib/worker-file.js +34 -0
- package/dist/cjs/lib/worker-file.js.map +1 -0
- package/dist/cjs/output/search-error.js +38 -0
- package/dist/cjs/output/search-error.js.map +1 -1
- package/dist/cjs/scan/pool.js +2 -23
- package/dist/cjs/scan/pool.js.map +1 -1
- package/dist/cjs/store/builder.d.cts +2 -1
- package/dist/cjs/store/builder.d.ts +2 -1
- package/dist/cjs/store/builder.js +4 -4
- package/dist/cjs/store/builder.js.map +1 -1
- package/dist/cjs/store/duckdb/lexical.d.cts +1 -0
- package/dist/cjs/store/duckdb/lexical.d.ts +1 -0
- package/dist/cjs/store/duckdb/lexical.js +28 -9
- package/dist/cjs/store/duckdb/lexical.js.map +1 -1
- package/dist/cjs/store/duckdb/native.d.cts +2 -0
- package/dist/cjs/store/duckdb/native.d.ts +2 -0
- package/dist/cjs/store/duckdb/native.js +11 -1
- package/dist/cjs/store/duckdb/native.js.map +1 -1
- package/dist/cjs/store/duckdb/ordered-bm25.d.cts +17 -0
- package/dist/cjs/store/duckdb/ordered-bm25.d.ts +17 -0
- package/dist/cjs/store/duckdb/ordered-bm25.js +238 -0
- package/dist/cjs/store/duckdb/ordered-bm25.js.map +1 -0
- package/dist/cjs/store/lock-wait.d.cts +2 -2
- package/dist/cjs/store/lock-wait.d.ts +2 -2
- package/dist/cjs/store/lock-wait.js +14 -11
- package/dist/cjs/store/lock-wait.js.map +1 -1
- package/dist/cjs/store/native.d.cts +4 -0
- package/dist/cjs/store/native.d.ts +4 -0
- package/dist/cjs/store/native.js +64 -9
- package/dist/cjs/store/native.js.map +1 -1
- package/dist/cjs/store/open.js +11 -7
- package/dist/cjs/store/open.js.map +1 -1
- package/dist/cjs/store/reconcile.d.cts +5 -1
- package/dist/cjs/store/reconcile.d.ts +5 -1
- package/dist/cjs/store/reconcile.js +5 -4
- package/dist/cjs/store/reconcile.js.map +1 -1
- package/dist/cjs/store/turso/connection.js +2 -2
- package/dist/cjs/store/turso/connection.js.map +1 -1
- package/dist/cjs/store/turso/lexical.js +5 -1
- package/dist/cjs/store/turso/lexical.js.map +1 -1
- package/dist/cjs/store/turso/reconcile.js +7 -6
- package/dist/cjs/store/turso/reconcile.js.map +1 -1
- package/dist/cjs/text/segment.js +168 -3
- package/dist/cjs/text/segment.js.map +1 -1
- package/dist/cjs/watch-claim.d.cts +39 -0
- package/dist/cjs/watch-claim.d.ts +39 -0
- package/dist/cjs/watch-claim.js +446 -0
- package/dist/cjs/watch-claim.js.map +1 -0
- package/dist/cjs/watch.js +240 -141
- package/dist/cjs/watch.js.map +1 -1
- package/dist/cjs/workers/watch-heartbeat.d.cts +1 -0
- package/dist/cjs/workers/watch-heartbeat.d.ts +1 -0
- package/dist/cjs/workers/watch-heartbeat.js +308 -0
- package/dist/cjs/workers/watch-heartbeat.js.map +1 -0
- package/dist/esm/cli/status.js +6 -5
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/commands/search.js +21 -0
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/lib/worker-file.d.ts +1 -0
- package/dist/esm/lib/worker-file.js +24 -0
- package/dist/esm/lib/worker-file.js.map +1 -0
- package/dist/esm/output/search-error.js +21 -0
- package/dist/esm/output/search-error.js.map +1 -1
- package/dist/esm/scan/pool.js +2 -22
- package/dist/esm/scan/pool.js.map +1 -1
- package/dist/esm/store/builder.d.ts +2 -1
- package/dist/esm/store/builder.js +4 -4
- package/dist/esm/store/builder.js.map +1 -1
- package/dist/esm/store/duckdb/lexical.d.ts +1 -0
- package/dist/esm/store/duckdb/lexical.js +16 -7
- package/dist/esm/store/duckdb/lexical.js.map +1 -1
- package/dist/esm/store/duckdb/native.d.ts +2 -0
- package/dist/esm/store/duckdb/native.js +5 -1
- package/dist/esm/store/duckdb/native.js.map +1 -1
- package/dist/esm/store/duckdb/ordered-bm25.d.ts +17 -0
- package/dist/esm/store/duckdb/ordered-bm25.js +56 -0
- package/dist/esm/store/duckdb/ordered-bm25.js.map +1 -0
- package/dist/esm/store/lock-wait.d.ts +2 -2
- package/dist/esm/store/lock-wait.js +17 -14
- package/dist/esm/store/lock-wait.js.map +1 -1
- package/dist/esm/store/native.d.ts +4 -0
- package/dist/esm/store/native.js +60 -6
- package/dist/esm/store/native.js.map +1 -1
- package/dist/esm/store/open.js +9 -6
- package/dist/esm/store/open.js.map +1 -1
- package/dist/esm/store/reconcile.d.ts +5 -1
- package/dist/esm/store/reconcile.js +4 -3
- package/dist/esm/store/reconcile.js.map +1 -1
- package/dist/esm/store/turso/connection.js +1 -1
- package/dist/esm/store/turso/connection.js.map +1 -1
- package/dist/esm/store/turso/lexical.js +5 -1
- package/dist/esm/store/turso/lexical.js.map +1 -1
- package/dist/esm/store/turso/reconcile.js +8 -5
- package/dist/esm/store/turso/reconcile.js.map +1 -1
- package/dist/esm/text/segment.js +130 -4
- package/dist/esm/text/segment.js.map +1 -1
- package/dist/esm/watch-claim.d.ts +39 -0
- package/dist/esm/watch-claim.js +176 -0
- package/dist/esm/watch-claim.js.map +1 -0
- package/dist/esm/watch.js +168 -117
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workers/watch-heartbeat.d.ts +1 -0
- package/dist/esm/workers/watch-heartbeat.js +106 -0
- package/dist/esm/workers/watch-heartbeat.js.map +1 -0
- package/package.json +2 -2
- package/skills/sense/SKILL.md +105 -104
- package/skills/sense/references/search.md +47 -0
- package/skills/sense/references/sql.md +120 -0
- package/skills/sense/references/stores/duckdb.md +35 -0
- package/skills/sense/references/stores/sqlite.md +66 -0
- package/skills/sense/references/stores/turso.md +36 -0
- package/skills/sense-setup/SKILL.md +93 -34
- package/skills/sense-setup/references/embeddings.md +51 -0
- package/skills/sense-setup/references/store-benchmarks.md +16 -0
- package/skills/sense-setup/references/stores/duckdb.md +28 -0
- package/skills/sense-setup/references/stores/sqlite.md +21 -0
- package/skills/sense-setup/references/stores/turso.md +25 -0
package/dist/esm/text/segment.js
CHANGED
|
@@ -6,7 +6,8 @@ import { stemmer } from 'stemmer';
|
|
|
6
6
|
export const UNSPACED_SCRIPTS = '\\p{scx=Han}\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Thai}\\p{scx=Khmer}\\p{scx=Lao}\\p{scx=Myanmar}';
|
|
7
7
|
// A run is script BASE characters with their combining marks attached; a bare mark after a
|
|
8
8
|
// Latin letter (decomposed é) never starts one.
|
|
9
|
-
const
|
|
9
|
+
const RUN_BODY = `(?:[${UNSPACED_SCRIPTS}]\\p{M}*)+`;
|
|
10
|
+
const RUN = new RegExp(`(${RUN_BODY})`, 'gu');
|
|
10
11
|
const HAS_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');
|
|
11
12
|
// Whether text holds a script that marks no word boundaries, the predicate every store's
|
|
12
13
|
// sidecar population and query split turns on.
|
|
@@ -121,26 +122,150 @@ const QUALIFIER = /(^|[\s(])(-?)(title|summary|text)\s*:\s*$/;
|
|
|
121
122
|
// Raw title/summary/text drop punctuation as unicode61's token separator, so `数数` and `数。数`
|
|
122
123
|
// falsely match there; only the barriered `_seg` columns are safe from it, hence this fallback target.
|
|
123
124
|
const SIDECAR_COLUMNS = '{title_seg summary_seg text_seg}:';
|
|
125
|
+
const AUTHORED_COLUMNS = '{title summary text}:';
|
|
126
|
+
const QUOTED_RUN_TOKEN = new RegExp(`"(${RUN_BODY})"`, 'gu');
|
|
127
|
+
const FTS_WORD = /[\p{L}\p{N}_]/u;
|
|
128
|
+
function isFtsWord(char) {
|
|
129
|
+
return char !== undefined && FTS_WORD.test(char);
|
|
130
|
+
}
|
|
131
|
+
function quoteEnd(text, start) {
|
|
132
|
+
for(let i = start + 1; i < text.length; i++){
|
|
133
|
+
if (text[i] !== '"') continue;
|
|
134
|
+
if (text[i + 1] === '"') {
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
return i + 1;
|
|
139
|
+
}
|
|
140
|
+
return text.length;
|
|
141
|
+
}
|
|
142
|
+
function nearEnd(text, open) {
|
|
143
|
+
let depth = 1;
|
|
144
|
+
let quoted = false;
|
|
145
|
+
for(let i = open + 1; i < text.length; i++){
|
|
146
|
+
const char = text[i];
|
|
147
|
+
if (char === '"') {
|
|
148
|
+
if (quoted && text[i + 1] === '"') {
|
|
149
|
+
i++;
|
|
150
|
+
} else {
|
|
151
|
+
quoted = !quoted;
|
|
152
|
+
}
|
|
153
|
+
} else if (!quoted && char === '(') {
|
|
154
|
+
depth++;
|
|
155
|
+
} else if (!quoted && char === ')' && --depth === 0) {
|
|
156
|
+
return i + 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return text.length;
|
|
160
|
+
}
|
|
161
|
+
// Lexical routing only: SQLite remains responsible for validating the expression. The scanner
|
|
162
|
+
// recognizes NEAR groups and the one FTS string governed by ^, while quoted text shields both.
|
|
163
|
+
function positionalSpans(terms) {
|
|
164
|
+
const spans = [];
|
|
165
|
+
for(let i = 0; i < terms.length; i++){
|
|
166
|
+
if (terms[i] === '"') {
|
|
167
|
+
i = quoteEnd(terms, i) - 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (terms.startsWith('NEAR', i) && !isFtsWord(terms[i - 1])) {
|
|
171
|
+
var _terms_open;
|
|
172
|
+
let open = i + 4;
|
|
173
|
+
while(/\s/u.test((_terms_open = terms[open]) !== null && _terms_open !== void 0 ? _terms_open : ''))open++;
|
|
174
|
+
if (terms[open] === '(') {
|
|
175
|
+
const end = nearEnd(terms, open);
|
|
176
|
+
if (HAS_RUN.test(terms.slice(open + 1, end))) spans.push({
|
|
177
|
+
start: i,
|
|
178
|
+
end,
|
|
179
|
+
qualified: QUALIFIER.test(terms.slice(0, i))
|
|
180
|
+
});
|
|
181
|
+
i = end - 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (terms[i] === '^' && !isFtsWord(terms[i - 1])) {
|
|
186
|
+
var _terms_operand;
|
|
187
|
+
let operand = i + 1;
|
|
188
|
+
while(/\s/u.test((_terms_operand = terms[operand]) !== null && _terms_operand !== void 0 ? _terms_operand : ''))operand++;
|
|
189
|
+
const end = terms[operand] === '"' ? quoteEnd(terms, operand) : (()=>{
|
|
190
|
+
var _terms_cursor;
|
|
191
|
+
let cursor = operand;
|
|
192
|
+
while(/[\p{L}\p{N}_]/u.test((_terms_cursor = terms[cursor]) !== null && _terms_cursor !== void 0 ? _terms_cursor : ''))cursor++;
|
|
193
|
+
return cursor;
|
|
194
|
+
})();
|
|
195
|
+
if (end > operand && HAS_RUN.test(terms.slice(operand, end))) spans.push({
|
|
196
|
+
start: i,
|
|
197
|
+
end,
|
|
198
|
+
qualified: QUALIFIER.test(terms.slice(0, i))
|
|
199
|
+
});
|
|
200
|
+
i = Math.max(i, end - 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return spans;
|
|
204
|
+
}
|
|
205
|
+
function routePositionalSpans(terms) {
|
|
206
|
+
const spans = positionalSpans(terms);
|
|
207
|
+
if (spans.length === 0) return terms;
|
|
208
|
+
let out = '';
|
|
209
|
+
let cursor = 0;
|
|
210
|
+
for (const span of spans){
|
|
211
|
+
out += terms.slice(cursor, span.start);
|
|
212
|
+
if (!span.qualified) out += AUTHORED_COLUMNS;
|
|
213
|
+
out += terms.slice(span.start, span.end);
|
|
214
|
+
cursor = span.end;
|
|
215
|
+
}
|
|
216
|
+
return out + terms.slice(cursor);
|
|
217
|
+
}
|
|
218
|
+
function inPositionalSpan(start, spans) {
|
|
219
|
+
return spans.some((span)=>start >= span.start && start < span.end);
|
|
220
|
+
}
|
|
124
221
|
// A run's punctuation-free groups, each its own quoted phrase (bare token if one grapheme),
|
|
125
222
|
// space-joined -- the same split points segmentField barriers, so query and index agree.
|
|
126
|
-
function runQuery(run, columnPrefix) {
|
|
127
|
-
return splitOnPunctuation(run).map((g)=>`${columnPrefix}${g.length > 1 ? `"${g.join(' ')}"` : g[0]}`).join(' ');
|
|
223
|
+
function runQuery(run, columnPrefix, quoteSingle = false) {
|
|
224
|
+
return splitOnPunctuation(run).map((g)=>`${columnPrefix}${g.length > 1 || quoteSingle ? `"${g.join(' ')}"` : g[0]}`).join(' ');
|
|
225
|
+
}
|
|
226
|
+
// Retarget complete quoted run tokens without interpreting the expression around them. Positional
|
|
227
|
+
// spans have already been routed to authored columns, so sidecar barriers cannot change meaning.
|
|
228
|
+
function quotedRunTokens(terms) {
|
|
229
|
+
const spans = positionalSpans(terms);
|
|
230
|
+
let out = '';
|
|
231
|
+
let cursor = 0;
|
|
232
|
+
for (const match of terms.matchAll(QUOTED_RUN_TOKEN)){
|
|
233
|
+
const start = match.index;
|
|
234
|
+
const run = match[1];
|
|
235
|
+
if (inPositionalSpan(start, spans) || PUNCTUATION.test(run)) continue;
|
|
236
|
+
out += terms.slice(cursor, start);
|
|
237
|
+
const qualifier = out.match(QUALIFIER);
|
|
238
|
+
if (qualifier) {
|
|
239
|
+
out = `${out.slice(0, qualifier.index)}${qualifier[1]}${runQuery(run, `${qualifier[2]}${qualifier[3]}_seg:`, true)}`;
|
|
240
|
+
} else {
|
|
241
|
+
out += runQuery(run, SIDECAR_COLUMNS, true);
|
|
242
|
+
}
|
|
243
|
+
cursor = start + match[0].length;
|
|
244
|
+
}
|
|
245
|
+
return cursor === 0 ? terms : out + terms.slice(cursor);
|
|
128
246
|
}
|
|
129
247
|
// Query side: each unspaced run becomes phrases of its graphemes, matching segmentField. A
|
|
130
248
|
// qualifier maps to its `_seg` column; unqualified maps to all three (SIDECAR_COLUMNS).
|
|
131
249
|
export function segmentMatch(terms) {
|
|
132
250
|
if (!HAS_RUN.test(terms)) return terms;
|
|
251
|
+
terms = routePositionalSpans(terms);
|
|
252
|
+
terms = quotedRunTokens(terms);
|
|
253
|
+
const spans = positionalSpans(terms);
|
|
133
254
|
let out = '';
|
|
134
255
|
let quoted = false;
|
|
256
|
+
let offset = 0;
|
|
135
257
|
const pieces = terms.split(RUN); // split keeps captured runs at odd indices
|
|
136
258
|
for(let i = 0; i < pieces.length; i++){
|
|
137
259
|
if (i % 2 === 0) {
|
|
138
260
|
for (const ch of pieces[i])if (ch === '"') quoted = !quoted;
|
|
139
261
|
out += pieces[i];
|
|
262
|
+
offset += pieces[i].length;
|
|
140
263
|
continue;
|
|
141
264
|
}
|
|
142
|
-
|
|
265
|
+
const protectedRun = inPositionalSpan(offset, spans);
|
|
266
|
+
if (quoted || protectedRun) {
|
|
143
267
|
out += pieces[i]; // an author's phrase is matched as written
|
|
268
|
+
offset += pieces[i].length;
|
|
144
269
|
continue;
|
|
145
270
|
}
|
|
146
271
|
const m = out.match(QUALIFIER);
|
|
@@ -149,6 +274,7 @@ export function segmentMatch(terms) {
|
|
|
149
274
|
} else {
|
|
150
275
|
out += runQuery(pieces[i], SIDECAR_COLUMNS);
|
|
151
276
|
}
|
|
277
|
+
offset += pieces[i].length;
|
|
152
278
|
}
|
|
153
279
|
return out;
|
|
154
280
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/text/segment.ts"],"sourcesContent":["import { stemmer } from 'stemmer';\n\n// Word boundaries FTS5's tokenizers cannot find on their own. Contract: seg(query) must appear in\n// seg(document) for any substring query; word mode is context-dependent and breaks that, so grapheme mode (UAX #29) is used.\n\n// Scripts written without word spaces: a closed set of writing systems.\n// Script_Extensions, not Script: the katakana long-vowel mark ー is Script=Common.\nexport const UNSPACED_SCRIPTS = '\\\\p{scx=Han}\\\\p{scx=Hiragana}\\\\p{scx=Katakana}\\\\p{scx=Thai}\\\\p{scx=Khmer}\\\\p{scx=Lao}\\\\p{scx=Myanmar}';\n// A run is script BASE characters with their combining marks attached; a bare mark after a\n// Latin letter (decomposed é) never starts one.\nconst RUN = new RegExp(`((?:[${UNSPACED_SCRIPTS}]\\\\p{M}*)+)`, 'gu');\nconst HAS_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');\n\n// Whether text holds a script that marks no word boundaries, the predicate every store's\n// sidecar population and query split turns on.\nexport function hasUnspacedRun(text: string): boolean {\n return HAS_RUN.test(text);\n}\n\nexport interface SearchToken {\n text: string;\n start: number;\n end: number;\n unspaced: boolean;\n}\n\n// Search tokens share the native separator contract: letters, marks, and numbers form tokens;\n// underscore, apostrophe, and hyphen separate them. Unspaced-script runs stay whole so callers\n// can apply substring semantics, while adjacent Latin text still gets its own token.\nexport function searchTokens(text: string): SearchToken[] {\n const tokens: SearchToken[] = [];\n for (const match of text.matchAll(/[\\p{L}\\p{N}][\\p{L}\\p{M}\\p{N}]*/gu)) {\n const start = match.index;\n const end = start + match[0].length;\n let cursor = start;\n for (const run of text.slice(start, end).matchAll(RUN)) {\n const runStart = start + run.index;\n if (runStart > cursor) tokens.push({ text: text.slice(cursor, runStart), start: cursor, end: runStart, unspaced: false });\n const runEnd = runStart + run[0].length;\n tokens.push({ text: run[0], start: runStart, end: runEnd, unspaced: true });\n cursor = runEnd;\n }\n if (cursor < end) tokens.push({ text: text.slice(cursor, end), start: cursor, end, unspaced: false });\n }\n return tokens;\n}\n\n// Phrase membership is checked after native ranking. A CJK token matches inside one authored\n// run; Latin tokens compare folded Porter stems. Each call scans one field once per candidate.\nexport function matchesSearchPhrase(text: string, phrase: SearchToken[]): boolean {\n const haystack = searchTokens(text);\n if (phrase.length === 0 || haystack.length < phrase.length) return false;\n for (let start = 0; start <= haystack.length - phrase.length; start++) {\n let matches = true;\n for (let i = 0; i < phrase.length; i++) {\n const query = phrase[i];\n const authored = haystack[start + i];\n if (query.unspaced) {\n if (!authored.unspaced || !authored.text.includes(query.text)) matches = false;\n } else if (authored.unspaced || stemmer(foldForSearch(authored.text)) !== stemmer(foldForSearch(query.text))) {\n matches = false;\n }\n if (!matches) break;\n }\n if (matches) return true;\n }\n return false;\n}\n\n// Character spans of unspaced-script runs in text, the same boundary the index segments on --\n// the snippet marker uses this to keep substring semantics inside a run and word/stem matching outside it.\nexport function unspacedRuns(text: string): Array<{ start: number; end: number }> {\n return Array.from(text.matchAll(RUN), (m) => ({ start: m.index, end: m.index + m[0].length }));\n}\n// Grapheme clusters, ECMA-402/UAX #29: base char plus its marks, ZWJ sequences, Hangul jamo.\n// Built on first use and kept: construction is 6.6 ms, and a tree with no unspaced-script run\n// never segments at all, so no command pays it at module load.\nlet graphemeSegmenter: Intl.Segmenter | undefined;\n// unicode61 drops Unicode punctuation as a separator. Some of it (。、「」) has Script_Extensions\n// into an unspaced script, so RUN keeps it -- a grapheme matching this becomes a split point.\nconst PUNCTUATION = /\\p{P}/u;\n// Token barrier between separate runs (or a punctuation split within one) so their graphemes\n// are never phrase-adjacent. U+A7F7: a letter (so FTS5 keeps it as a token) no one types.\nconst BARRIER = 'ꟷ';\n\nfunction graphemes(run: string): string[] {\n graphemeSegmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n return Array.from(graphemeSegmenter.segment(run), (s) => s.segment);\n}\n\n// A run's graphemes, cut into punctuation-free groups at every punctuation grapheme (dropped,\n// matching unicode61). The one place punctuation is classified, so index and query agree.\nfunction splitOnPunctuation(run: string): string[][] {\n const groups: string[][] = [[]];\n for (const g of graphemes(run)) {\n if (PUNCTUATION.test(g)) groups.push([]);\n else groups[groups.length - 1].push(g);\n }\n return groups.filter((g) => g.length > 0);\n}\n\n// Index side: '' when the field has no unspaced-script run. Otherwise each run explodes into\n// its graphemes, barrier-delimited from its neighbors and from a punctuation split within itself.\nexport function segmentField(text: string): string {\n if (!HAS_RUN.test(text)) return '';\n const out = text.replace(RUN, (run) => {\n const body = splitOnPunctuation(run)\n .map((g) => g.join(' '))\n .join(` ${BARRIER} `);\n return ` ${BARRIER} ${body} ${BARRIER} `;\n });\n return out.replace(/\\s+/g, ' ').trim();\n}\n\n// A `title:`/`summary:`/`text:` qualifier directly before a run that is about to become a\n// quoted grapheme phrase, so the rewrite can retarget it at the matching `_seg` column.\nconst QUALIFIER = /(^|[\\s(])(-?)(title|summary|text)\\s*:\\s*$/;\n\n// Raw title/summary/text drop punctuation as unicode61's token separator, so `数数` and `数。数`\n// falsely match there; only the barriered `_seg` columns are safe from it, hence this fallback target.\nconst SIDECAR_COLUMNS = '{title_seg summary_seg text_seg}:';\n\n// A run's punctuation-free groups, each its own quoted phrase (bare token if one grapheme),\n// space-joined -- the same split points segmentField barriers, so query and index agree.\nfunction runQuery(run: string, columnPrefix: string): string {\n return splitOnPunctuation(run)\n .map((g) => `${columnPrefix}${g.length > 1 ? `\"${g.join(' ')}\"` : g[0]}`)\n .join(' ');\n}\n\n// Query side: each unspaced run becomes phrases of its graphemes, matching segmentField. A\n// qualifier maps to its `_seg` column; unqualified maps to all three (SIDECAR_COLUMNS).\nexport function segmentMatch(terms: string): string {\n if (!HAS_RUN.test(terms)) return terms;\n let out = '';\n let quoted = false;\n const pieces = terms.split(RUN); // split keeps captured runs at odd indices\n for (let i = 0; i < pieces.length; i++) {\n if (i % 2 === 0) {\n for (const ch of pieces[i]) if (ch === '\"') quoted = !quoted;\n out += pieces[i];\n continue;\n }\n if (quoted) {\n out += pieces[i]; // an author's phrase is matched as written\n continue;\n }\n const m = out.match(QUALIFIER);\n if (m) {\n out = `${out.slice(0, m.index)}${m[1]}${runQuery(pieces[i], `${m[2]}${m[3]}_seg:`)}`;\n } else {\n out += runQuery(pieces[i], SIDECAR_COLUMNS);\n }\n }\n return out;\n}\n\n// FTS tokenizers compare letters without combining marks. Keep the authored string for output and\n// offsets, but use this key when JS needs to compare a matched word to its source segment.\nexport function foldForSearch(text: string): string {\n return text.normalize('NFD').replace(/\\p{M}/gu, '').toLowerCase();\n}\n"],"names":["stemmer","UNSPACED_SCRIPTS","RUN","RegExp","HAS_RUN","hasUnspacedRun","text","test","searchTokens","tokens","match","matchAll","start","index","end","length","cursor","run","slice","runStart","push","unspaced","runEnd","matchesSearchPhrase","phrase","haystack","matches","i","query","authored","includes","foldForSearch","unspacedRuns","Array","from","m","graphemeSegmenter","PUNCTUATION","BARRIER","graphemes","Intl","Segmenter","undefined","granularity","segment","s","splitOnPunctuation","groups","g","filter","segmentField","out","replace","body","map","join","trim","QUALIFIER","SIDECAR_COLUMNS","runQuery","columnPrefix","segmentMatch","terms","quoted","pieces","split","ch","normalize","toLowerCase"],"mappings":"AAAA,SAASA,OAAO,QAAQ,UAAU;AAElC,kGAAkG;AAClG,6HAA6H;AAE7H,wEAAwE;AACxE,kFAAkF;AAClF,OAAO,MAAMC,mBAAmB,wGAAwG;AACxI,2FAA2F;AAC3F,gDAAgD;AAChD,MAAMC,MAAM,IAAIC,OAAO,CAAC,KAAK,EAAEF,iBAAiB,WAAW,CAAC,EAAE;AAC9D,MAAMG,UAAU,IAAID,OAAO,CAAC,CAAC,EAAEF,iBAAiB,CAAC,CAAC,EAAE;AAEpD,yFAAyF;AACzF,+CAA+C;AAC/C,OAAO,SAASI,eAAeC,IAAY;IACzC,OAAOF,QAAQG,IAAI,CAACD;AACtB;AASA,8FAA8F;AAC9F,+FAA+F;AAC/F,qFAAqF;AACrF,OAAO,SAASE,aAAaF,IAAY;IACvC,MAAMG,SAAwB,EAAE;IAChC,KAAK,MAAMC,SAASJ,KAAKK,QAAQ,CAAC,oCAAqC;QACrE,MAAMC,QAAQF,MAAMG,KAAK;QACzB,MAAMC,MAAMF,QAAQF,KAAK,CAAC,EAAE,CAACK,MAAM;QACnC,IAAIC,SAASJ;QACb,KAAK,MAAMK,OAAOX,KAAKY,KAAK,CAACN,OAAOE,KAAKH,QAAQ,CAACT,KAAM;YACtD,MAAMiB,WAAWP,QAAQK,IAAIJ,KAAK;YAClC,IAAIM,WAAWH,QAAQP,OAAOW,IAAI,CAAC;gBAAEd,MAAMA,KAAKY,KAAK,CAACF,QAAQG;gBAAWP,OAAOI;gBAAQF,KAAKK;gBAAUE,UAAU;YAAM;YACvH,MAAMC,SAASH,WAAWF,GAAG,CAAC,EAAE,CAACF,MAAM;YACvCN,OAAOW,IAAI,CAAC;gBAAEd,MAAMW,GAAG,CAAC,EAAE;gBAAEL,OAAOO;gBAAUL,KAAKQ;gBAAQD,UAAU;YAAK;YACzEL,SAASM;QACX;QACA,IAAIN,SAASF,KAAKL,OAAOW,IAAI,CAAC;YAAEd,MAAMA,KAAKY,KAAK,CAACF,QAAQF;YAAMF,OAAOI;YAAQF;YAAKO,UAAU;QAAM;IACrG;IACA,OAAOZ;AACT;AAEA,6FAA6F;AAC7F,+FAA+F;AAC/F,OAAO,SAASc,oBAAoBjB,IAAY,EAAEkB,MAAqB;IACrE,MAAMC,WAAWjB,aAAaF;IAC9B,IAAIkB,OAAOT,MAAM,KAAK,KAAKU,SAASV,MAAM,GAAGS,OAAOT,MAAM,EAAE,OAAO;IACnE,IAAK,IAAIH,QAAQ,GAAGA,SAASa,SAASV,MAAM,GAAGS,OAAOT,MAAM,EAAEH,QAAS;QACrE,IAAIc,UAAU;QACd,IAAK,IAAIC,IAAI,GAAGA,IAAIH,OAAOT,MAAM,EAAEY,IAAK;YACtC,MAAMC,QAAQJ,MAAM,CAACG,EAAE;YACvB,MAAME,WAAWJ,QAAQ,CAACb,QAAQe,EAAE;YACpC,IAAIC,MAAMP,QAAQ,EAAE;gBAClB,IAAI,CAACQ,SAASR,QAAQ,IAAI,CAACQ,SAASvB,IAAI,CAACwB,QAAQ,CAACF,MAAMtB,IAAI,GAAGoB,UAAU;YAC3E,OAAO,IAAIG,SAASR,QAAQ,IAAIrB,QAAQ+B,cAAcF,SAASvB,IAAI,OAAON,QAAQ+B,cAAcH,MAAMtB,IAAI,IAAI;gBAC5GoB,UAAU;YACZ;YACA,IAAI,CAACA,SAAS;QAChB;QACA,IAAIA,SAAS,OAAO;IACtB;IACA,OAAO;AACT;AAEA,8FAA8F;AAC9F,2GAA2G;AAC3G,OAAO,SAASM,aAAa1B,IAAY;IACvC,OAAO2B,MAAMC,IAAI,CAAC5B,KAAKK,QAAQ,CAACT,MAAM,CAACiC,IAAO,CAAA;YAAEvB,OAAOuB,EAAEtB,KAAK;YAAEC,KAAKqB,EAAEtB,KAAK,GAAGsB,CAAC,CAAC,EAAE,CAACpB,MAAM;QAAC,CAAA;AAC7F;AACA,6FAA6F;AAC7F,8FAA8F;AAC9F,+DAA+D;AAC/D,IAAIqB;AACJ,8FAA8F;AAC9F,8FAA8F;AAC9F,MAAMC,cAAc;AACpB,6FAA6F;AAC7F,0FAA0F;AAC1F,MAAMC,UAAU;AAEhB,SAASC,UAAUtB,GAAW;IAC5BmB,8BAAAA,+BAAAA,oBAAAA,oBAAsB,IAAII,KAAKC,SAAS,CAACC,WAAW;QAAEC,aAAa;IAAW;IAC9E,OAAOV,MAAMC,IAAI,CAACE,kBAAkBQ,OAAO,CAAC3B,MAAM,CAAC4B,IAAMA,EAAED,OAAO;AACpE;AAEA,8FAA8F;AAC9F,0FAA0F;AAC1F,SAASE,mBAAmB7B,GAAW;IACrC,MAAM8B,SAAqB;QAAC,EAAE;KAAC;IAC/B,KAAK,MAAMC,KAAKT,UAAUtB,KAAM;QAC9B,IAAIoB,YAAY9B,IAAI,CAACyC,IAAID,OAAO3B,IAAI,CAAC,EAAE;aAClC2B,MAAM,CAACA,OAAOhC,MAAM,GAAG,EAAE,CAACK,IAAI,CAAC4B;IACtC;IACA,OAAOD,OAAOE,MAAM,CAAC,CAACD,IAAMA,EAAEjC,MAAM,GAAG;AACzC;AAEA,6FAA6F;AAC7F,kGAAkG;AAClG,OAAO,SAASmC,aAAa5C,IAAY;IACvC,IAAI,CAACF,QAAQG,IAAI,CAACD,OAAO,OAAO;IAChC,MAAM6C,MAAM7C,KAAK8C,OAAO,CAAClD,KAAK,CAACe;QAC7B,MAAMoC,OAAOP,mBAAmB7B,KAC7BqC,GAAG,CAAC,CAACN,IAAMA,EAAEO,IAAI,CAAC,MAClBA,IAAI,CAAC,CAAC,CAAC,EAAEjB,QAAQ,CAAC,CAAC;QACtB,OAAO,CAAC,CAAC,EAAEA,QAAQ,CAAC,EAAEe,KAAK,CAAC,EAAEf,QAAQ,CAAC,CAAC;IAC1C;IACA,OAAOa,IAAIC,OAAO,CAAC,QAAQ,KAAKI,IAAI;AACtC;AAEA,0FAA0F;AAC1F,wFAAwF;AACxF,MAAMC,YAAY;AAElB,4FAA4F;AAC5F,uGAAuG;AACvG,MAAMC,kBAAkB;AAExB,4FAA4F;AAC5F,yFAAyF;AACzF,SAASC,SAAS1C,GAAW,EAAE2C,YAAoB;IACjD,OAAOd,mBAAmB7B,KACvBqC,GAAG,CAAC,CAACN,IAAM,GAAGY,eAAeZ,EAAEjC,MAAM,GAAG,IAAI,CAAC,CAAC,EAAEiC,EAAEO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAGP,CAAC,CAAC,EAAE,EAAE,EACvEO,IAAI,CAAC;AACV;AAEA,2FAA2F;AAC3F,wFAAwF;AACxF,OAAO,SAASM,aAAaC,KAAa;IACxC,IAAI,CAAC1D,QAAQG,IAAI,CAACuD,QAAQ,OAAOA;IACjC,IAAIX,MAAM;IACV,IAAIY,SAAS;IACb,MAAMC,SAASF,MAAMG,KAAK,CAAC/D,MAAM,2CAA2C;IAC5E,IAAK,IAAIyB,IAAI,GAAGA,IAAIqC,OAAOjD,MAAM,EAAEY,IAAK;QACtC,IAAIA,IAAI,MAAM,GAAG;YACf,KAAK,MAAMuC,MAAMF,MAAM,CAACrC,EAAE,CAAE,IAAIuC,OAAO,KAAKH,SAAS,CAACA;YACtDZ,OAAOa,MAAM,CAACrC,EAAE;YAChB;QACF;QACA,IAAIoC,QAAQ;YACVZ,OAAOa,MAAM,CAACrC,EAAE,EAAE,2CAA2C;YAC7D;QACF;QACA,MAAMQ,IAAIgB,IAAIzC,KAAK,CAAC+C;QACpB,IAAItB,GAAG;YACLgB,MAAM,GAAGA,IAAIjC,KAAK,CAAC,GAAGiB,EAAEtB,KAAK,IAAIsB,CAAC,CAAC,EAAE,GAAGwB,SAASK,MAAM,CAACrC,EAAE,EAAE,GAAGQ,CAAC,CAAC,EAAE,GAAGA,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG;QACtF,OAAO;YACLgB,OAAOQ,SAASK,MAAM,CAACrC,EAAE,EAAE+B;QAC7B;IACF;IACA,OAAOP;AACT;AAEA,kGAAkG;AAClG,2FAA2F;AAC3F,OAAO,SAASpB,cAAczB,IAAY;IACxC,OAAOA,KAAK6D,SAAS,CAAC,OAAOf,OAAO,CAAC,WAAW,IAAIgB,WAAW;AACjE"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/text/segment.ts"],"sourcesContent":["import { stemmer } from 'stemmer';\n\n// Word boundaries FTS5's tokenizers cannot find on their own. Contract: seg(query) must appear in\n// seg(document) for any substring query; word mode is context-dependent and breaks that, so grapheme mode (UAX #29) is used.\n\n// Scripts written without word spaces: a closed set of writing systems.\n// Script_Extensions, not Script: the katakana long-vowel mark ー is Script=Common.\nexport const UNSPACED_SCRIPTS = '\\\\p{scx=Han}\\\\p{scx=Hiragana}\\\\p{scx=Katakana}\\\\p{scx=Thai}\\\\p{scx=Khmer}\\\\p{scx=Lao}\\\\p{scx=Myanmar}';\n// A run is script BASE characters with their combining marks attached; a bare mark after a\n// Latin letter (decomposed é) never starts one.\nconst RUN_BODY = `(?:[${UNSPACED_SCRIPTS}]\\\\p{M}*)+`;\nconst RUN = new RegExp(`(${RUN_BODY})`, 'gu');\nconst HAS_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');\n\n// Whether text holds a script that marks no word boundaries, the predicate every store's\n// sidecar population and query split turns on.\nexport function hasUnspacedRun(text: string): boolean {\n return HAS_RUN.test(text);\n}\n\nexport interface SearchToken {\n text: string;\n start: number;\n end: number;\n unspaced: boolean;\n}\n\n// Search tokens share the native separator contract: letters, marks, and numbers form tokens;\n// underscore, apostrophe, and hyphen separate them. Unspaced-script runs stay whole so callers\n// can apply substring semantics, while adjacent Latin text still gets its own token.\nexport function searchTokens(text: string): SearchToken[] {\n const tokens: SearchToken[] = [];\n for (const match of text.matchAll(/[\\p{L}\\p{N}][\\p{L}\\p{M}\\p{N}]*/gu)) {\n const start = match.index;\n const end = start + match[0].length;\n let cursor = start;\n for (const run of text.slice(start, end).matchAll(RUN)) {\n const runStart = start + run.index;\n if (runStart > cursor) tokens.push({ text: text.slice(cursor, runStart), start: cursor, end: runStart, unspaced: false });\n const runEnd = runStart + run[0].length;\n tokens.push({ text: run[0], start: runStart, end: runEnd, unspaced: true });\n cursor = runEnd;\n }\n if (cursor < end) tokens.push({ text: text.slice(cursor, end), start: cursor, end, unspaced: false });\n }\n return tokens;\n}\n\n// Phrase membership is checked after native ranking. A CJK token matches inside one authored\n// run; Latin tokens compare folded Porter stems. Each call scans one field once per candidate.\nexport function matchesSearchPhrase(text: string, phrase: SearchToken[]): boolean {\n const haystack = searchTokens(text);\n if (phrase.length === 0 || haystack.length < phrase.length) return false;\n for (let start = 0; start <= haystack.length - phrase.length; start++) {\n let matches = true;\n for (let i = 0; i < phrase.length; i++) {\n const query = phrase[i];\n const authored = haystack[start + i];\n if (query.unspaced) {\n if (!authored.unspaced || !authored.text.includes(query.text)) matches = false;\n } else if (authored.unspaced || stemmer(foldForSearch(authored.text)) !== stemmer(foldForSearch(query.text))) {\n matches = false;\n }\n if (!matches) break;\n }\n if (matches) return true;\n }\n return false;\n}\n\n// Character spans of unspaced-script runs in text, the same boundary the index segments on --\n// the snippet marker uses this to keep substring semantics inside a run and word/stem matching outside it.\nexport function unspacedRuns(text: string): Array<{ start: number; end: number }> {\n return Array.from(text.matchAll(RUN), (m) => ({ start: m.index, end: m.index + m[0].length }));\n}\n// Grapheme clusters, ECMA-402/UAX #29: base char plus its marks, ZWJ sequences, Hangul jamo.\n// Built on first use and kept: construction is 6.6 ms, and a tree with no unspaced-script run\n// never segments at all, so no command pays it at module load.\nlet graphemeSegmenter: Intl.Segmenter | undefined;\n// unicode61 drops Unicode punctuation as a separator. Some of it (。、「」) has Script_Extensions\n// into an unspaced script, so RUN keeps it -- a grapheme matching this becomes a split point.\nconst PUNCTUATION = /\\p{P}/u;\n// Token barrier between separate runs (or a punctuation split within one) so their graphemes\n// are never phrase-adjacent. U+A7F7: a letter (so FTS5 keeps it as a token) no one types.\nconst BARRIER = 'ꟷ';\n\nfunction graphemes(run: string): string[] {\n graphemeSegmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n return Array.from(graphemeSegmenter.segment(run), (s) => s.segment);\n}\n\n// A run's graphemes, cut into punctuation-free groups at every punctuation grapheme (dropped,\n// matching unicode61). The one place punctuation is classified, so index and query agree.\nfunction splitOnPunctuation(run: string): string[][] {\n const groups: string[][] = [[]];\n for (const g of graphemes(run)) {\n if (PUNCTUATION.test(g)) groups.push([]);\n else groups[groups.length - 1].push(g);\n }\n return groups.filter((g) => g.length > 0);\n}\n\n// Index side: '' when the field has no unspaced-script run. Otherwise each run explodes into\n// its graphemes, barrier-delimited from its neighbors and from a punctuation split within itself.\nexport function segmentField(text: string): string {\n if (!HAS_RUN.test(text)) return '';\n const out = text.replace(RUN, (run) => {\n const body = splitOnPunctuation(run)\n .map((g) => g.join(' '))\n .join(` ${BARRIER} `);\n return ` ${BARRIER} ${body} ${BARRIER} `;\n });\n return out.replace(/\\s+/g, ' ').trim();\n}\n\n// A `title:`/`summary:`/`text:` qualifier directly before a run that is about to become a\n// quoted grapheme phrase, so the rewrite can retarget it at the matching `_seg` column.\nconst QUALIFIER = /(^|[\\s(])(-?)(title|summary|text)\\s*:\\s*$/;\n\n// Raw title/summary/text drop punctuation as unicode61's token separator, so `数数` and `数。数`\n// falsely match there; only the barriered `_seg` columns are safe from it, hence this fallback target.\nconst SIDECAR_COLUMNS = '{title_seg summary_seg text_seg}:';\nconst AUTHORED_COLUMNS = '{title summary text}:';\nconst QUOTED_RUN_TOKEN = new RegExp(`\"(${RUN_BODY})\"`, 'gu');\n\ninterface PositionalSpan {\n start: number;\n end: number;\n qualified: boolean;\n}\n\nconst FTS_WORD = /[\\p{L}\\p{N}_]/u;\n\nfunction isFtsWord(char: string | undefined): boolean {\n return char !== undefined && FTS_WORD.test(char);\n}\n\nfunction quoteEnd(text: string, start: number): number {\n for (let i = start + 1; i < text.length; i++) {\n if (text[i] !== '\"') continue;\n if (text[i + 1] === '\"') {\n i++;\n continue;\n }\n return i + 1;\n }\n return text.length;\n}\n\nfunction nearEnd(text: string, open: number): number {\n let depth = 1;\n let quoted = false;\n for (let i = open + 1; i < text.length; i++) {\n const char = text[i];\n if (char === '\"') {\n if (quoted && text[i + 1] === '\"') {\n i++;\n } else {\n quoted = !quoted;\n }\n } else if (!quoted && char === '(') {\n depth++;\n } else if (!quoted && char === ')' && --depth === 0) {\n return i + 1;\n }\n }\n return text.length;\n}\n\n// Lexical routing only: SQLite remains responsible for validating the expression. The scanner\n// recognizes NEAR groups and the one FTS string governed by ^, while quoted text shields both.\nfunction positionalSpans(terms: string): PositionalSpan[] {\n const spans: PositionalSpan[] = [];\n for (let i = 0; i < terms.length; i++) {\n if (terms[i] === '\"') {\n i = quoteEnd(terms, i) - 1;\n continue;\n }\n if (terms.startsWith('NEAR', i) && !isFtsWord(terms[i - 1])) {\n let open = i + 4;\n while (/\\s/u.test(terms[open] ?? '')) open++;\n if (terms[open] === '(') {\n const end = nearEnd(terms, open);\n if (HAS_RUN.test(terms.slice(open + 1, end))) spans.push({ start: i, end, qualified: QUALIFIER.test(terms.slice(0, i)) });\n i = end - 1;\n continue;\n }\n }\n if (terms[i] === '^' && !isFtsWord(terms[i - 1])) {\n let operand = i + 1;\n while (/\\s/u.test(terms[operand] ?? '')) operand++;\n const end =\n terms[operand] === '\"'\n ? quoteEnd(terms, operand)\n : (() => {\n let cursor = operand;\n while (/[\\p{L}\\p{N}_]/u.test(terms[cursor] ?? '')) cursor++;\n return cursor;\n })();\n if (end > operand && HAS_RUN.test(terms.slice(operand, end))) spans.push({ start: i, end, qualified: QUALIFIER.test(terms.slice(0, i)) });\n i = Math.max(i, end - 1);\n }\n }\n return spans;\n}\n\nfunction routePositionalSpans(terms: string): string {\n const spans = positionalSpans(terms);\n if (spans.length === 0) return terms;\n let out = '';\n let cursor = 0;\n for (const span of spans) {\n out += terms.slice(cursor, span.start);\n if (!span.qualified) out += AUTHORED_COLUMNS;\n out += terms.slice(span.start, span.end);\n cursor = span.end;\n }\n return out + terms.slice(cursor);\n}\n\nfunction inPositionalSpan(start: number, spans: PositionalSpan[]): boolean {\n return spans.some((span) => start >= span.start && start < span.end);\n}\n\n// A run's punctuation-free groups, each its own quoted phrase (bare token if one grapheme),\n// space-joined -- the same split points segmentField barriers, so query and index agree.\nfunction runQuery(run: string, columnPrefix: string, quoteSingle = false): string {\n return splitOnPunctuation(run)\n .map((g) => `${columnPrefix}${g.length > 1 || quoteSingle ? `\"${g.join(' ')}\"` : g[0]}`)\n .join(' ');\n}\n\n// Retarget complete quoted run tokens without interpreting the expression around them. Positional\n// spans have already been routed to authored columns, so sidecar barriers cannot change meaning.\nfunction quotedRunTokens(terms: string): string {\n const spans = positionalSpans(terms);\n let out = '';\n let cursor = 0;\n for (const match of terms.matchAll(QUOTED_RUN_TOKEN)) {\n const start = match.index;\n const run = match[1];\n if (inPositionalSpan(start, spans) || PUNCTUATION.test(run)) continue;\n out += terms.slice(cursor, start);\n const qualifier = out.match(QUALIFIER);\n if (qualifier) {\n out = `${out.slice(0, qualifier.index)}${qualifier[1]}${runQuery(run, `${qualifier[2]}${qualifier[3]}_seg:`, true)}`;\n } else {\n out += runQuery(run, SIDECAR_COLUMNS, true);\n }\n cursor = start + match[0].length;\n }\n return cursor === 0 ? terms : out + terms.slice(cursor);\n}\n\n// Query side: each unspaced run becomes phrases of its graphemes, matching segmentField. A\n// qualifier maps to its `_seg` column; unqualified maps to all three (SIDECAR_COLUMNS).\nexport function segmentMatch(terms: string): string {\n if (!HAS_RUN.test(terms)) return terms;\n terms = routePositionalSpans(terms);\n terms = quotedRunTokens(terms);\n const spans = positionalSpans(terms);\n let out = '';\n let quoted = false;\n let offset = 0;\n const pieces = terms.split(RUN); // split keeps captured runs at odd indices\n for (let i = 0; i < pieces.length; i++) {\n if (i % 2 === 0) {\n for (const ch of pieces[i]) if (ch === '\"') quoted = !quoted;\n out += pieces[i];\n offset += pieces[i].length;\n continue;\n }\n const protectedRun = inPositionalSpan(offset, spans);\n if (quoted || protectedRun) {\n out += pieces[i]; // an author's phrase is matched as written\n offset += pieces[i].length;\n continue;\n }\n const m = out.match(QUALIFIER);\n if (m) {\n out = `${out.slice(0, m.index)}${m[1]}${runQuery(pieces[i], `${m[2]}${m[3]}_seg:`)}`;\n } else {\n out += runQuery(pieces[i], SIDECAR_COLUMNS);\n }\n offset += pieces[i].length;\n }\n return out;\n}\n\n// FTS tokenizers compare letters without combining marks. Keep the authored string for output and\n// offsets, but use this key when JS needs to compare a matched word to its source segment.\nexport function foldForSearch(text: string): string {\n return text.normalize('NFD').replace(/\\p{M}/gu, '').toLowerCase();\n}\n"],"names":["stemmer","UNSPACED_SCRIPTS","RUN_BODY","RUN","RegExp","HAS_RUN","hasUnspacedRun","text","test","searchTokens","tokens","match","matchAll","start","index","end","length","cursor","run","slice","runStart","push","unspaced","runEnd","matchesSearchPhrase","phrase","haystack","matches","i","query","authored","includes","foldForSearch","unspacedRuns","Array","from","m","graphemeSegmenter","PUNCTUATION","BARRIER","graphemes","Intl","Segmenter","undefined","granularity","segment","s","splitOnPunctuation","groups","g","filter","segmentField","out","replace","body","map","join","trim","QUALIFIER","SIDECAR_COLUMNS","AUTHORED_COLUMNS","QUOTED_RUN_TOKEN","FTS_WORD","isFtsWord","char","quoteEnd","nearEnd","open","depth","quoted","positionalSpans","terms","spans","startsWith","qualified","operand","Math","max","routePositionalSpans","span","inPositionalSpan","some","runQuery","columnPrefix","quoteSingle","quotedRunTokens","qualifier","segmentMatch","offset","pieces","split","ch","protectedRun","normalize","toLowerCase"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type WorkerErrorPayload } from './scan/worker-error.js';
|
|
2
|
+
export declare const WATCH_CLAIM_FILENAME = ".sense-watch.db";
|
|
3
|
+
export declare const WATCH_HEARTBEAT_INTERVAL_MS = 5000;
|
|
4
|
+
export declare const WATCH_STALE_HEARTBEAT_MS = 15000;
|
|
5
|
+
export interface WatchClaimRecord {
|
|
6
|
+
token: string;
|
|
7
|
+
pid: number;
|
|
8
|
+
heartbeatMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface WatchClaimWorkerData {
|
|
11
|
+
configDir: string;
|
|
12
|
+
force: boolean;
|
|
13
|
+
heartbeatIntervalMs: number;
|
|
14
|
+
pid: number;
|
|
15
|
+
token: string;
|
|
16
|
+
}
|
|
17
|
+
export type WatchClaimWorkerMessage = {
|
|
18
|
+
type: 'acquired';
|
|
19
|
+
} | {
|
|
20
|
+
type: 'stopped';
|
|
21
|
+
} | {
|
|
22
|
+
type: 'failure';
|
|
23
|
+
error: WorkerErrorPayload;
|
|
24
|
+
};
|
|
25
|
+
export interface WatchClaimController {
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare class WatchClaimDatabase {
|
|
29
|
+
private readonly db;
|
|
30
|
+
private readonly conn;
|
|
31
|
+
constructor(configDir: string);
|
|
32
|
+
acquire(token: string, pid: number, force: boolean): Promise<void>;
|
|
33
|
+
renew(token: string): boolean;
|
|
34
|
+
release(token: string): boolean;
|
|
35
|
+
read(): WatchClaimRecord | null;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
export declare function readWatchClaim(configDir: string): WatchClaimRecord | null;
|
|
39
|
+
export declare function startWatchClaim(configDir: string, force: boolean, heartbeatIntervalMs: number, onFailure: (err: Error) => void): Promise<WatchClaimController>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { Worker } from 'node:worker_threads';
|
|
6
|
+
import { SenseError } from './errors.js';
|
|
7
|
+
import { resolveWorkerFile } from './lib/worker-file.js';
|
|
8
|
+
import { reviveError } from './scan/worker-error.js';
|
|
9
|
+
import { createConnection } from './store/sqlite/connection.js';
|
|
10
|
+
import { BEGIN_WRITE, withTransaction } from './store/transaction.js';
|
|
11
|
+
export const WATCH_CLAIM_FILENAME = '.sense-watch.db';
|
|
12
|
+
export const WATCH_HEARTBEAT_INTERVAL_MS = 5000;
|
|
13
|
+
export const WATCH_STALE_HEARTBEAT_MS = 15000;
|
|
14
|
+
const CLAIM_BUSY_TIMEOUT_MS = 30000;
|
|
15
|
+
function claimPath(configDir) {
|
|
16
|
+
return join(configDir, WATCH_CLAIM_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
function changedOne(changes) {
|
|
19
|
+
return changes === 1 || changes === BigInt(1);
|
|
20
|
+
}
|
|
21
|
+
// This database is separate from the selected search store so its heartbeat remains writable while
|
|
22
|
+
// a DuckDB, Turso, or SQLite reconciliation owns the cache connection or blocks the main thread.
|
|
23
|
+
export class WatchClaimDatabase {
|
|
24
|
+
async acquire(token, pid, force) {
|
|
25
|
+
await withTransaction(this.conn, async ()=>{
|
|
26
|
+
const current = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get();
|
|
27
|
+
if (current && !force) {
|
|
28
|
+
const age = Date.now() - current.heartbeat_ms;
|
|
29
|
+
if (age >= 0 && age < WATCH_STALE_HEARTBEAT_MS) throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);
|
|
30
|
+
}
|
|
31
|
+
this.db.prepare('INSERT INTO watch_claim (singleton, token, pid, heartbeat_ms) VALUES (1, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET token = excluded.token, pid = excluded.pid, heartbeat_ms = excluded.heartbeat_ms').run(token, pid, Date.now());
|
|
32
|
+
}, BEGIN_WRITE);
|
|
33
|
+
}
|
|
34
|
+
renew(token) {
|
|
35
|
+
return changedOne(this.db.prepare('UPDATE watch_claim SET heartbeat_ms = ? WHERE singleton = 1 AND token = ?').run(Date.now(), token).changes);
|
|
36
|
+
}
|
|
37
|
+
release(token) {
|
|
38
|
+
return changedOne(this.db.prepare('DELETE FROM watch_claim WHERE singleton = 1 AND token = ?').run(token).changes);
|
|
39
|
+
}
|
|
40
|
+
read() {
|
|
41
|
+
const row = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get();
|
|
42
|
+
return row ? {
|
|
43
|
+
token: row.token,
|
|
44
|
+
pid: row.pid,
|
|
45
|
+
heartbeatMs: row.heartbeat_ms
|
|
46
|
+
} : null;
|
|
47
|
+
}
|
|
48
|
+
close() {
|
|
49
|
+
this.db.close();
|
|
50
|
+
}
|
|
51
|
+
constructor(configDir){
|
|
52
|
+
const db = new DatabaseSync(claimPath(configDir));
|
|
53
|
+
try {
|
|
54
|
+
db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);
|
|
55
|
+
db.exec('CREATE TABLE IF NOT EXISTS watch_claim (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), token TEXT NOT NULL, pid INTEGER NOT NULL, heartbeat_ms INTEGER NOT NULL)');
|
|
56
|
+
} catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
db.close();
|
|
59
|
+
} catch (closeError) {
|
|
60
|
+
throw new AggregateError([
|
|
61
|
+
err,
|
|
62
|
+
closeError
|
|
63
|
+
], 'watch claim setup and database cleanup both failed');
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
this.db = db;
|
|
68
|
+
this.conn = createConnection(db);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Status opens the coordinator read-only and therefore cannot create either the file or its schema.
|
|
72
|
+
export function readWatchClaim(configDir) {
|
|
73
|
+
const path = claimPath(configDir);
|
|
74
|
+
if (!existsSync(path)) return null;
|
|
75
|
+
let db;
|
|
76
|
+
try {
|
|
77
|
+
db = new DatabaseSync(path, {
|
|
78
|
+
readOnly: true
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (!existsSync(path)) return null;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);
|
|
86
|
+
const row = db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get();
|
|
87
|
+
return row ? {
|
|
88
|
+
token: row.token,
|
|
89
|
+
pid: row.pid,
|
|
90
|
+
heartbeatMs: row.heartbeat_ms
|
|
91
|
+
} : null;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (/no such table: watch_claim/.test(err.message)) return null;
|
|
94
|
+
throw err;
|
|
95
|
+
} finally{
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function startWatchClaim(configDir, force, heartbeatIntervalMs, onFailure) {
|
|
100
|
+
const workerData = {
|
|
101
|
+
configDir,
|
|
102
|
+
force,
|
|
103
|
+
heartbeatIntervalMs,
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
token: randomUUID()
|
|
106
|
+
};
|
|
107
|
+
const worker = new Worker(resolveWorkerFile('watch-heartbeat'), {
|
|
108
|
+
workerData
|
|
109
|
+
});
|
|
110
|
+
let acquired = false;
|
|
111
|
+
let closing = false;
|
|
112
|
+
let exited = false;
|
|
113
|
+
let failureReported = false;
|
|
114
|
+
let closeError = null;
|
|
115
|
+
let closePromise;
|
|
116
|
+
let resolveExit;
|
|
117
|
+
const exit = new Promise((resolve)=>{
|
|
118
|
+
resolveExit = resolve;
|
|
119
|
+
});
|
|
120
|
+
let resolveStart;
|
|
121
|
+
let rejectStart;
|
|
122
|
+
const start = new Promise((resolve, reject)=>{
|
|
123
|
+
resolveStart = resolve;
|
|
124
|
+
rejectStart = reject;
|
|
125
|
+
});
|
|
126
|
+
const fail = (err)=>{
|
|
127
|
+
if (failureReported) return;
|
|
128
|
+
failureReported = true;
|
|
129
|
+
if (!acquired) rejectStart(err);
|
|
130
|
+
else if (closing) closeError !== null && closeError !== void 0 ? closeError : closeError = err;
|
|
131
|
+
else onFailure(err);
|
|
132
|
+
};
|
|
133
|
+
worker.on('message', (message)=>{
|
|
134
|
+
if (message.type === 'acquired') {
|
|
135
|
+
acquired = true;
|
|
136
|
+
resolveStart();
|
|
137
|
+
} else if (message.type === 'failure') {
|
|
138
|
+
fail(reviveError(message.error));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
worker.on('error', fail);
|
|
142
|
+
worker.on('exit', (code)=>{
|
|
143
|
+
exited = true;
|
|
144
|
+
resolveExit();
|
|
145
|
+
if (closing && code !== 0) closeError !== null && closeError !== void 0 ? closeError : closeError = new Error(`watch heartbeat worker stopped with exit code ${code}`);
|
|
146
|
+
else if (!closing && !failureReported) fail(new Error(`watch heartbeat worker stopped unexpectedly with exit code ${code}`));
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
await start;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
closing = true;
|
|
152
|
+
if (!exited) await worker.terminate();
|
|
153
|
+
await exit;
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
close () {
|
|
158
|
+
closePromise !== null && closePromise !== void 0 ? closePromise : closePromise = (async ()=>{
|
|
159
|
+
closing = true;
|
|
160
|
+
if (!exited) {
|
|
161
|
+
try {
|
|
162
|
+
worker.postMessage({
|
|
163
|
+
type: 'stop'
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
closeError !== null && closeError !== void 0 ? closeError : closeError = err instanceof Error ? err : new Error(String(err));
|
|
167
|
+
await worker.terminate();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await exit;
|
|
171
|
+
if (closeError) throw closeError;
|
|
172
|
+
})();
|
|
173
|
+
return closePromise;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch-claim.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport { Worker } from 'node:worker_threads';\nimport { SenseError } from './errors.ts';\nimport { resolveWorkerFile } from './lib/worker-file.ts';\nimport { reviveError, type WorkerErrorPayload } from './scan/worker-error.ts';\nimport { createConnection } from './store/sqlite/connection.ts';\nimport { BEGIN_WRITE, withTransaction } from './store/transaction.ts';\n\nexport const WATCH_CLAIM_FILENAME = '.sense-watch.db';\nexport const WATCH_HEARTBEAT_INTERVAL_MS = 5_000;\nexport const WATCH_STALE_HEARTBEAT_MS = 15_000;\nconst CLAIM_BUSY_TIMEOUT_MS = 30_000;\n\nexport interface WatchClaimRecord {\n token: string;\n pid: number;\n heartbeatMs: number;\n}\n\ninterface ClaimRow {\n token: string;\n pid: number;\n heartbeat_ms: number;\n}\n\nexport interface WatchClaimWorkerData {\n configDir: string;\n force: boolean;\n heartbeatIntervalMs: number;\n pid: number;\n token: string;\n}\n\nexport type WatchClaimWorkerMessage = { type: 'acquired' } | { type: 'stopped' } | { type: 'failure'; error: WorkerErrorPayload };\n\nexport interface WatchClaimController {\n close(): Promise<void>;\n}\n\nfunction claimPath(configDir: string): string {\n return join(configDir, WATCH_CLAIM_FILENAME);\n}\n\nfunction changedOne(changes: number | bigint): boolean {\n return changes === 1 || changes === BigInt(1);\n}\n\n// This database is separate from the selected search store so its heartbeat remains writable while\n// a DuckDB, Turso, or SQLite reconciliation owns the cache connection or blocks the main thread.\nexport class WatchClaimDatabase {\n private readonly db: DatabaseSync;\n private readonly conn: ReturnType<typeof createConnection>;\n\n constructor(configDir: string) {\n const db = new DatabaseSync(claimPath(configDir));\n try {\n db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);\n db.exec('CREATE TABLE IF NOT EXISTS watch_claim (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), token TEXT NOT NULL, pid INTEGER NOT NULL, heartbeat_ms INTEGER NOT NULL)');\n } catch (err) {\n try {\n db.close();\n } catch (closeError) {\n throw new AggregateError([err, closeError], 'watch claim setup and database cleanup both failed');\n }\n throw err;\n }\n this.db = db;\n this.conn = createConnection(db);\n }\n\n async acquire(token: string, pid: number, force: boolean): Promise<void> {\n await withTransaction(\n this.conn,\n async () => {\n const current = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get() as ClaimRow | undefined;\n if (current && !force) {\n const age = Date.now() - current.heartbeat_ms;\n if (age >= 0 && age < WATCH_STALE_HEARTBEAT_MS) throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n this.db.prepare('INSERT INTO watch_claim (singleton, token, pid, heartbeat_ms) VALUES (1, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET token = excluded.token, pid = excluded.pid, heartbeat_ms = excluded.heartbeat_ms').run(token, pid, Date.now());\n },\n BEGIN_WRITE\n );\n }\n\n renew(token: string): boolean {\n return changedOne(this.db.prepare('UPDATE watch_claim SET heartbeat_ms = ? WHERE singleton = 1 AND token = ?').run(Date.now(), token).changes);\n }\n\n release(token: string): boolean {\n return changedOne(this.db.prepare('DELETE FROM watch_claim WHERE singleton = 1 AND token = ?').run(token).changes);\n }\n\n read(): WatchClaimRecord | null {\n const row = this.db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get() as ClaimRow | undefined;\n return row ? { token: row.token, pid: row.pid, heartbeatMs: row.heartbeat_ms } : null;\n }\n\n close(): void {\n this.db.close();\n }\n}\n\n// Status opens the coordinator read-only and therefore cannot create either the file or its schema.\nexport function readWatchClaim(configDir: string): WatchClaimRecord | null {\n const path = claimPath(configDir);\n if (!existsSync(path)) return null;\n let db: DatabaseSync;\n try {\n db = new DatabaseSync(path, { readOnly: true });\n } catch (err) {\n if (!existsSync(path)) return null;\n throw err;\n }\n try {\n db.exec(`PRAGMA busy_timeout = ${CLAIM_BUSY_TIMEOUT_MS}`);\n const row = db.prepare('SELECT token, pid, heartbeat_ms FROM watch_claim WHERE singleton = 1').get() as ClaimRow | undefined;\n return row ? { token: row.token, pid: row.pid, heartbeatMs: row.heartbeat_ms } : null;\n } catch (err) {\n if (/no such table: watch_claim/.test((err as Error).message)) return null;\n throw err;\n } finally {\n db.close();\n }\n}\n\nexport async function startWatchClaim(configDir: string, force: boolean, heartbeatIntervalMs: number, onFailure: (err: Error) => void): Promise<WatchClaimController> {\n const workerData: WatchClaimWorkerData = { configDir, force, heartbeatIntervalMs, pid: process.pid, token: randomUUID() };\n const worker = new Worker(resolveWorkerFile('watch-heartbeat'), { workerData });\n let acquired = false;\n let closing = false;\n let exited = false;\n let failureReported = false;\n let closeError: Error | null = null;\n let closePromise: Promise<void> | undefined;\n let resolveExit!: () => void;\n const exit = new Promise<void>((resolve) => {\n resolveExit = resolve;\n });\n let resolveStart!: () => void;\n let rejectStart!: (err: Error) => void;\n const start = new Promise<void>((resolve, reject) => {\n resolveStart = resolve;\n rejectStart = reject;\n });\n\n const fail = (err: Error): void => {\n if (failureReported) return;\n failureReported = true;\n if (!acquired) rejectStart(err);\n else if (closing) closeError ??= err;\n else onFailure(err);\n };\n worker.on('message', (message: WatchClaimWorkerMessage) => {\n if (message.type === 'acquired') {\n acquired = true;\n resolveStart();\n } else if (message.type === 'failure') {\n fail(reviveError(message.error));\n }\n });\n worker.on('error', fail);\n worker.on('exit', (code) => {\n exited = true;\n resolveExit();\n if (closing && code !== 0) closeError ??= new Error(`watch heartbeat worker stopped with exit code ${code}`);\n else if (!closing && !failureReported) fail(new Error(`watch heartbeat worker stopped unexpectedly with exit code ${code}`));\n });\n\n try {\n await start;\n } catch (err) {\n closing = true;\n if (!exited) await worker.terminate();\n await exit;\n throw err;\n }\n\n return {\n close(): Promise<void> {\n closePromise ??= (async () => {\n closing = true;\n if (!exited) {\n try {\n worker.postMessage({ type: 'stop' });\n } catch (err) {\n closeError ??= err instanceof Error ? err : new Error(String(err));\n await worker.terminate();\n }\n }\n await exit;\n if (closeError) throw closeError;\n })();\n return closePromise;\n },\n };\n}\n"],"names":["randomUUID","existsSync","join","DatabaseSync","Worker","SenseError","resolveWorkerFile","reviveError","createConnection","BEGIN_WRITE","withTransaction","WATCH_CLAIM_FILENAME","WATCH_HEARTBEAT_INTERVAL_MS","WATCH_STALE_HEARTBEAT_MS","CLAIM_BUSY_TIMEOUT_MS","claimPath","configDir","changedOne","changes","BigInt","WatchClaimDatabase","acquire","token","pid","force","conn","current","db","prepare","get","age","Date","now","heartbeat_ms","Math","round","run","renew","release","read","row","heartbeatMs","close","exec","err","closeError","AggregateError","readWatchClaim","path","readOnly","test","message","startWatchClaim","heartbeatIntervalMs","onFailure","workerData","process","worker","acquired","closing","exited","failureReported","closePromise","resolveExit","exit","Promise","resolve","resolveStart","rejectStart","start","reject","fail","on","type","error","code","Error","terminate","postMessage","String"],"mappings":"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"}
|