@hatk/hatk 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/backfill.d.ts +11 -0
  2. package/dist/backfill.d.ts.map +1 -0
  3. package/dist/backfill.js +328 -0
  4. package/dist/car.d.ts +5 -0
  5. package/dist/car.d.ts.map +1 -0
  6. package/dist/car.js +52 -0
  7. package/dist/cbor.d.ts +7 -0
  8. package/dist/cbor.d.ts.map +1 -0
  9. package/dist/cbor.js +89 -0
  10. package/dist/cid.d.ts +4 -0
  11. package/dist/cid.d.ts.map +1 -0
  12. package/dist/cid.js +39 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +1663 -0
  16. package/dist/config.d.ts +47 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +43 -0
  19. package/dist/db.d.ts +134 -0
  20. package/dist/db.d.ts.map +1 -0
  21. package/dist/db.js +1361 -0
  22. package/dist/feeds.d.ts +95 -0
  23. package/dist/feeds.d.ts.map +1 -0
  24. package/dist/feeds.js +144 -0
  25. package/dist/fts.d.ts +20 -0
  26. package/dist/fts.d.ts.map +1 -0
  27. package/dist/fts.js +762 -0
  28. package/dist/hydrate.d.ts +23 -0
  29. package/dist/hydrate.d.ts.map +1 -0
  30. package/dist/hydrate.js +75 -0
  31. package/dist/indexer.d.ts +14 -0
  32. package/dist/indexer.d.ts.map +1 -0
  33. package/dist/indexer.js +316 -0
  34. package/dist/labels.d.ts +29 -0
  35. package/dist/labels.d.ts.map +1 -0
  36. package/dist/labels.js +111 -0
  37. package/dist/lex-types.d.ts +401 -0
  38. package/dist/lex-types.d.ts.map +1 -0
  39. package/dist/lex-types.js +4 -0
  40. package/dist/lexicon-resolve.d.ts +14 -0
  41. package/dist/lexicon-resolve.d.ts.map +1 -0
  42. package/dist/lexicon-resolve.js +280 -0
  43. package/dist/logger.d.ts +4 -0
  44. package/dist/logger.d.ts.map +1 -0
  45. package/dist/logger.js +23 -0
  46. package/dist/main.d.ts +3 -0
  47. package/dist/main.d.ts.map +1 -0
  48. package/dist/main.js +148 -0
  49. package/dist/mst.d.ts +6 -0
  50. package/dist/mst.d.ts.map +1 -0
  51. package/dist/mst.js +30 -0
  52. package/dist/oauth/client.d.ts +16 -0
  53. package/dist/oauth/client.d.ts.map +1 -0
  54. package/dist/oauth/client.js +54 -0
  55. package/dist/oauth/crypto.d.ts +28 -0
  56. package/dist/oauth/crypto.d.ts.map +1 -0
  57. package/dist/oauth/crypto.js +101 -0
  58. package/dist/oauth/db.d.ts +47 -0
  59. package/dist/oauth/db.d.ts.map +1 -0
  60. package/dist/oauth/db.js +139 -0
  61. package/dist/oauth/discovery.d.ts +22 -0
  62. package/dist/oauth/discovery.d.ts.map +1 -0
  63. package/dist/oauth/discovery.js +50 -0
  64. package/dist/oauth/dpop.d.ts +11 -0
  65. package/dist/oauth/dpop.d.ts.map +1 -0
  66. package/dist/oauth/dpop.js +56 -0
  67. package/dist/oauth/hooks.d.ts +10 -0
  68. package/dist/oauth/hooks.d.ts.map +1 -0
  69. package/dist/oauth/hooks.js +40 -0
  70. package/dist/oauth/server.d.ts +86 -0
  71. package/dist/oauth/server.d.ts.map +1 -0
  72. package/dist/oauth/server.js +572 -0
  73. package/dist/opengraph.d.ts +34 -0
  74. package/dist/opengraph.d.ts.map +1 -0
  75. package/dist/opengraph.js +198 -0
  76. package/dist/schema.d.ts +51 -0
  77. package/dist/schema.d.ts.map +1 -0
  78. package/dist/schema.js +358 -0
  79. package/dist/seed.d.ts +29 -0
  80. package/dist/seed.d.ts.map +1 -0
  81. package/dist/seed.js +86 -0
  82. package/dist/server.d.ts +6 -0
  83. package/dist/server.d.ts.map +1 -0
  84. package/dist/server.js +1024 -0
  85. package/dist/setup.d.ts +8 -0
  86. package/dist/setup.d.ts.map +1 -0
  87. package/dist/setup.js +48 -0
  88. package/dist/test-browser.d.ts +14 -0
  89. package/dist/test-browser.d.ts.map +1 -0
  90. package/dist/test-browser.js +26 -0
  91. package/dist/test.d.ts +47 -0
  92. package/dist/test.d.ts.map +1 -0
  93. package/dist/test.js +256 -0
  94. package/dist/views.d.ts +40 -0
  95. package/dist/views.d.ts.map +1 -0
  96. package/dist/views.js +178 -0
  97. package/dist/vite-plugin.d.ts +5 -0
  98. package/dist/vite-plugin.d.ts.map +1 -0
  99. package/dist/vite-plugin.js +86 -0
  100. package/dist/xrpc-client.d.ts +18 -0
  101. package/dist/xrpc-client.d.ts.map +1 -0
  102. package/dist/xrpc-client.js +54 -0
  103. package/dist/xrpc.d.ts +53 -0
  104. package/dist/xrpc.d.ts.map +1 -0
  105. package/dist/xrpc.js +139 -0
  106. package/fonts/Inter-Regular.woff +0 -0
  107. package/package.json +41 -0
  108. package/public/admin-auth.js +320 -0
  109. package/public/admin.html +2166 -0
package/dist/fts.js ADDED
@@ -0,0 +1,762 @@
1
+ import { getSchema, runSQL } from "./db.js";
2
+ import { getLexicon } from "./schema.js";
3
+ import { emit, timer } from "./logger.js";
4
+ /**
5
+ * Resolve a lexicon ref like "#artist" to its definition.
6
+ * Only handles local refs (same lexicon).
7
+ */
8
+ function resolveRef(ref, lexicon) {
9
+ if (!ref.startsWith('#'))
10
+ return null;
11
+ const defName = ref.slice(1);
12
+ return lexicon.defs?.[defName] || null;
13
+ }
14
+ /**
15
+ * Given a JSON column and its lexicon property definition, produce
16
+ * search column expressions that extract searchable text.
17
+ */
18
+ function jsonSearchColumns(colName, prop, lexicon) {
19
+ const columns = [];
20
+ // Strip table qualifier (e.g. "t.artists" → "artists") for use in aliases
21
+ const aliasBase = colName.includes('.') ? colName.split('.').pop() : colName;
22
+ if (prop.type === 'array' && prop.items) {
23
+ const itemDef = prop.items.type === 'ref' && prop.items.ref ? resolveRef(prop.items.ref, lexicon) : prop.items;
24
+ if (!itemDef)
25
+ return columns;
26
+ if (itemDef.type === 'string') {
27
+ // array of strings — join into one text column
28
+ columns.push({
29
+ expr: `list_string_agg(json_extract_string(${colName}, '$[*]'))`,
30
+ alias: `${aliasBase}_text`,
31
+ });
32
+ }
33
+ else if (itemDef.type === 'object' && itemDef.properties) {
34
+ // array of objects — one column per string property
35
+ for (const [field, fieldProp] of Object.entries(itemDef.properties)) {
36
+ if (fieldProp.type === 'string') {
37
+ columns.push({
38
+ expr: `list_string_agg(json_extract_string(${colName}, '$[*].${field}'))`,
39
+ alias: `${aliasBase}_${field}`,
40
+ });
41
+ }
42
+ }
43
+ }
44
+ }
45
+ else if (prop.type === 'object' && prop.properties) {
46
+ // plain object — one column per string property
47
+ for (const [field, fieldProp] of Object.entries(prop.properties)) {
48
+ if (fieldProp.type === 'string') {
49
+ columns.push({
50
+ expr: `json_extract_string(${colName}, '$.${field}')`,
51
+ alias: `${aliasBase}_${field}`,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ // blob, union, unknown — skip (no useful text to extract)
57
+ return columns;
58
+ }
59
+ // Tracks when each collection's FTS index was last rebuilt
60
+ const lastRebuiltAt = new Map();
61
+ // Cache of search column metadata per collection, populated during buildFtsIndex
62
+ const searchColumnCache = new Map();
63
+ export function getSearchColumns(collection) {
64
+ return searchColumnCache.get(collection) || [];
65
+ }
66
+ export function getLastRebuiltAt(collection) {
67
+ return lastRebuiltAt.get(collection) ?? null;
68
+ }
69
+ /**
70
+ * DuckDB FTS can't handle dots in table names (interprets them as catalog.schema.table).
71
+ * We create a shadow table with underscored names for FTS indexing.
72
+ */
73
+ export function ftsTableName(collection) {
74
+ return '_fts_' + collection.replace(/\./g, '_');
75
+ }
76
+ /**
77
+ * Build FTS index for a collection.
78
+ * Creates a shadow table copy and indexes all TEXT NOT NULL columns
79
+ * using Porter stemmer with English stopwords.
80
+ */
81
+ export async function buildFtsIndex(collection) {
82
+ const schema = getSchema(collection);
83
+ if (!schema)
84
+ throw new Error(`Unknown collection: ${collection}`);
85
+ const lexicon = getLexicon(collection);
86
+ const record = lexicon?.defs?.main?.record;
87
+ // Build column list for shadow table
88
+ const selectExprs = ['t.uri', 't.cid', 't.did', 't.indexed_at'];
89
+ const searchColNames = [];
90
+ for (const col of schema.columns) {
91
+ if (col.duckdbType === 'TEXT') {
92
+ selectExprs.push(`t.${col.name}`);
93
+ searchColNames.push(col.name);
94
+ }
95
+ else if (col.duckdbType === 'JSON' && record?.properties) {
96
+ const prop = record.properties[col.originalName];
97
+ if (prop?.type === 'blob')
98
+ continue; // skip blobs
99
+ if (prop && lexicon) {
100
+ const derived = jsonSearchColumns(`t.${col.name}`, prop, lexicon);
101
+ if (derived.length > 0) {
102
+ for (const d of derived) {
103
+ selectExprs.push(`${d.expr} AS ${d.alias}`);
104
+ searchColNames.push(d.alias);
105
+ }
106
+ continue;
107
+ }
108
+ }
109
+ // Fallback: cast JSON to TEXT
110
+ selectExprs.push(`CAST(t.${col.name} AS TEXT) AS ${col.name}`);
111
+ searchColNames.push(col.name);
112
+ }
113
+ }
114
+ // Include searchable text from child tables (decomposed array fields)
115
+ for (const child of schema.children) {
116
+ for (const col of child.columns) {
117
+ if (col.duckdbType === 'TEXT') {
118
+ const alias = `${child.fieldName}_${col.name}`;
119
+ selectExprs.push(`(SELECT string_agg(c.${col.name}, ' ') FROM ${child.tableName} c WHERE c.parent_uri = t.uri) AS ${alias}`);
120
+ searchColNames.push(alias);
121
+ }
122
+ }
123
+ }
124
+ // Include searchable text from union branch tables
125
+ for (const union of schema.unions) {
126
+ for (const branch of union.branches) {
127
+ for (const col of branch.columns) {
128
+ if (col.duckdbType === 'TEXT') {
129
+ const alias = `${union.fieldName}_${branch.branchName}_${col.name}`;
130
+ selectExprs.push(`(SELECT string_agg(c.${col.name}, ' ') FROM ${branch.tableName} c WHERE c.parent_uri = t.uri) AS ${alias}`);
131
+ searchColNames.push(alias);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ // Include handle from _repos for people search
137
+ selectExprs.push('r.handle');
138
+ searchColNames.push('handle');
139
+ if (searchColNames.length === 0) {
140
+ return;
141
+ }
142
+ const safeName = ftsTableName(collection);
143
+ // Build shadow table with derived text columns, joining _repos for handle
144
+ await runSQL(`CREATE OR REPLACE TABLE ${safeName} AS SELECT ${selectExprs.join(', ')} FROM ${schema.tableName} t LEFT JOIN _repos r ON t.did = r.did`);
145
+ // Drop existing index (ignore error if none exists)
146
+ try {
147
+ await runSQL(`PRAGMA drop_fts_index('${safeName}')`);
148
+ }
149
+ catch { }
150
+ // Build FTS index over all search columns
151
+ const colList = searchColNames.map((c) => `'${c}'`).join(', ');
152
+ await runSQL(`PRAGMA create_fts_index('${safeName}', 'uri', ${colList}, stemmer='porter', stopwords='english', strip_accents=1, lower=1, overwrite=1)`);
153
+ searchColumnCache.set(collection, searchColNames);
154
+ lastRebuiltAt.set(collection, new Date().toISOString());
155
+ }
156
+ /**
157
+ * Rebuild FTS indexes for all registered collections.
158
+ */
159
+ // DuckDB's built-in English stop words (571 words) — must match stopwords='english' in create_fts_index
160
+ const ENGLISH_STOP_WORDS = new Set([
161
+ 'a',
162
+ "a's",
163
+ 'able',
164
+ 'about',
165
+ 'above',
166
+ 'according',
167
+ 'accordingly',
168
+ 'across',
169
+ 'actually',
170
+ 'after',
171
+ 'afterwards',
172
+ 'again',
173
+ 'against',
174
+ "ain't",
175
+ 'all',
176
+ 'allow',
177
+ 'allows',
178
+ 'almost',
179
+ 'alone',
180
+ 'along',
181
+ 'already',
182
+ 'also',
183
+ 'although',
184
+ 'always',
185
+ 'am',
186
+ 'among',
187
+ 'amongst',
188
+ 'an',
189
+ 'and',
190
+ 'another',
191
+ 'any',
192
+ 'anybody',
193
+ 'anyhow',
194
+ 'anyone',
195
+ 'anything',
196
+ 'anyway',
197
+ 'anyways',
198
+ 'anywhere',
199
+ 'apart',
200
+ 'appear',
201
+ 'appreciate',
202
+ 'appropriate',
203
+ 'are',
204
+ "aren't",
205
+ 'around',
206
+ 'as',
207
+ 'aside',
208
+ 'ask',
209
+ 'asking',
210
+ 'associated',
211
+ 'at',
212
+ 'available',
213
+ 'away',
214
+ 'awfully',
215
+ 'b',
216
+ 'be',
217
+ 'became',
218
+ 'because',
219
+ 'become',
220
+ 'becomes',
221
+ 'becoming',
222
+ 'been',
223
+ 'before',
224
+ 'beforehand',
225
+ 'behind',
226
+ 'being',
227
+ 'believe',
228
+ 'below',
229
+ 'beside',
230
+ 'besides',
231
+ 'best',
232
+ 'better',
233
+ 'between',
234
+ 'beyond',
235
+ 'both',
236
+ 'brief',
237
+ 'but',
238
+ 'by',
239
+ 'c',
240
+ "c'mon",
241
+ "c's",
242
+ 'came',
243
+ 'can',
244
+ "can't",
245
+ 'cannot',
246
+ 'cant',
247
+ 'cause',
248
+ 'causes',
249
+ 'certain',
250
+ 'certainly',
251
+ 'changes',
252
+ 'clearly',
253
+ 'co',
254
+ 'com',
255
+ 'come',
256
+ 'comes',
257
+ 'concerning',
258
+ 'consequently',
259
+ 'consider',
260
+ 'considering',
261
+ 'contain',
262
+ 'containing',
263
+ 'contains',
264
+ 'corresponding',
265
+ 'could',
266
+ "couldn't",
267
+ 'course',
268
+ 'currently',
269
+ 'd',
270
+ 'definitely',
271
+ 'described',
272
+ 'despite',
273
+ 'did',
274
+ "didn't",
275
+ 'different',
276
+ 'do',
277
+ 'does',
278
+ "doesn't",
279
+ 'doing',
280
+ "don't",
281
+ 'done',
282
+ 'down',
283
+ 'downwards',
284
+ 'during',
285
+ 'e',
286
+ 'each',
287
+ 'edu',
288
+ 'eg',
289
+ 'eight',
290
+ 'either',
291
+ 'else',
292
+ 'elsewhere',
293
+ 'enough',
294
+ 'entirely',
295
+ 'especially',
296
+ 'et',
297
+ 'etc',
298
+ 'even',
299
+ 'ever',
300
+ 'every',
301
+ 'everybody',
302
+ 'everyone',
303
+ 'everything',
304
+ 'everywhere',
305
+ 'ex',
306
+ 'exactly',
307
+ 'example',
308
+ 'except',
309
+ 'f',
310
+ 'far',
311
+ 'few',
312
+ 'fifth',
313
+ 'first',
314
+ 'five',
315
+ 'followed',
316
+ 'following',
317
+ 'follows',
318
+ 'for',
319
+ 'former',
320
+ 'formerly',
321
+ 'forth',
322
+ 'four',
323
+ 'from',
324
+ 'further',
325
+ 'furthermore',
326
+ 'g',
327
+ 'get',
328
+ 'gets',
329
+ 'getting',
330
+ 'given',
331
+ 'gives',
332
+ 'go',
333
+ 'goes',
334
+ 'going',
335
+ 'gone',
336
+ 'got',
337
+ 'gotten',
338
+ 'greetings',
339
+ 'h',
340
+ 'had',
341
+ "hadn't",
342
+ 'happens',
343
+ 'hardly',
344
+ 'has',
345
+ "hasn't",
346
+ 'have',
347
+ "haven't",
348
+ 'having',
349
+ 'he',
350
+ "he's",
351
+ 'hello',
352
+ 'help',
353
+ 'hence',
354
+ 'her',
355
+ 'here',
356
+ "here's",
357
+ 'hereafter',
358
+ 'hereby',
359
+ 'herein',
360
+ 'hereupon',
361
+ 'hers',
362
+ 'herself',
363
+ 'hi',
364
+ 'him',
365
+ 'himself',
366
+ 'his',
367
+ 'hither',
368
+ 'hopefully',
369
+ 'how',
370
+ 'howbeit',
371
+ 'however',
372
+ 'i',
373
+ "i'd",
374
+ "i'll",
375
+ "i'm",
376
+ "i've",
377
+ 'ie',
378
+ 'if',
379
+ 'ignored',
380
+ 'immediate',
381
+ 'in',
382
+ 'inasmuch',
383
+ 'inc',
384
+ 'indeed',
385
+ 'indicate',
386
+ 'indicated',
387
+ 'indicates',
388
+ 'inner',
389
+ 'insofar',
390
+ 'instead',
391
+ 'into',
392
+ 'inward',
393
+ 'is',
394
+ "isn't",
395
+ 'it',
396
+ "it'd",
397
+ "it'll",
398
+ "it's",
399
+ 'its',
400
+ 'itself',
401
+ 'j',
402
+ 'just',
403
+ 'k',
404
+ 'keep',
405
+ 'keeps',
406
+ 'kept',
407
+ 'know',
408
+ 'known',
409
+ 'knows',
410
+ 'l',
411
+ 'last',
412
+ 'lately',
413
+ 'later',
414
+ 'latter',
415
+ 'latterly',
416
+ 'least',
417
+ 'less',
418
+ 'lest',
419
+ 'let',
420
+ "let's",
421
+ 'like',
422
+ 'liked',
423
+ 'likely',
424
+ 'little',
425
+ 'look',
426
+ 'looking',
427
+ 'looks',
428
+ 'ltd',
429
+ 'm',
430
+ 'mainly',
431
+ 'many',
432
+ 'may',
433
+ 'maybe',
434
+ 'me',
435
+ 'mean',
436
+ 'meanwhile',
437
+ 'merely',
438
+ 'might',
439
+ 'more',
440
+ 'moreover',
441
+ 'most',
442
+ 'mostly',
443
+ 'much',
444
+ 'must',
445
+ 'my',
446
+ 'myself',
447
+ 'n',
448
+ 'name',
449
+ 'namely',
450
+ 'nd',
451
+ 'near',
452
+ 'nearly',
453
+ 'necessary',
454
+ 'need',
455
+ 'needs',
456
+ 'neither',
457
+ 'never',
458
+ 'nevertheless',
459
+ 'new',
460
+ 'next',
461
+ 'nine',
462
+ 'no',
463
+ 'nobody',
464
+ 'non',
465
+ 'none',
466
+ 'noone',
467
+ 'nor',
468
+ 'normally',
469
+ 'not',
470
+ 'nothing',
471
+ 'novel',
472
+ 'now',
473
+ 'nowhere',
474
+ 'o',
475
+ 'obviously',
476
+ 'of',
477
+ 'off',
478
+ 'often',
479
+ 'oh',
480
+ 'ok',
481
+ 'okay',
482
+ 'old',
483
+ 'on',
484
+ 'once',
485
+ 'one',
486
+ 'ones',
487
+ 'only',
488
+ 'onto',
489
+ 'or',
490
+ 'other',
491
+ 'others',
492
+ 'otherwise',
493
+ 'ought',
494
+ 'our',
495
+ 'ours',
496
+ 'ourselves',
497
+ 'out',
498
+ 'outside',
499
+ 'over',
500
+ 'overall',
501
+ 'own',
502
+ 'p',
503
+ 'particular',
504
+ 'particularly',
505
+ 'per',
506
+ 'perhaps',
507
+ 'placed',
508
+ 'please',
509
+ 'plus',
510
+ 'possible',
511
+ 'presumably',
512
+ 'probably',
513
+ 'provides',
514
+ 'q',
515
+ 'que',
516
+ 'quite',
517
+ 'qv',
518
+ 'r',
519
+ 'rather',
520
+ 'rd',
521
+ 're',
522
+ 'really',
523
+ 'reasonably',
524
+ 'regarding',
525
+ 'regardless',
526
+ 'regards',
527
+ 'relatively',
528
+ 'respectively',
529
+ 'right',
530
+ 's',
531
+ 'said',
532
+ 'same',
533
+ 'saw',
534
+ 'say',
535
+ 'saying',
536
+ 'says',
537
+ 'second',
538
+ 'secondly',
539
+ 'see',
540
+ 'seeing',
541
+ 'seem',
542
+ 'seemed',
543
+ 'seeming',
544
+ 'seems',
545
+ 'seen',
546
+ 'self',
547
+ 'selves',
548
+ 'sensible',
549
+ 'sent',
550
+ 'serious',
551
+ 'seriously',
552
+ 'seven',
553
+ 'several',
554
+ 'shall',
555
+ 'she',
556
+ 'should',
557
+ "shouldn't",
558
+ 'since',
559
+ 'six',
560
+ 'so',
561
+ 'some',
562
+ 'somebody',
563
+ 'somehow',
564
+ 'someone',
565
+ 'something',
566
+ 'sometime',
567
+ 'sometimes',
568
+ 'somewhat',
569
+ 'somewhere',
570
+ 'soon',
571
+ 'sorry',
572
+ 'specified',
573
+ 'specify',
574
+ 'specifying',
575
+ 'still',
576
+ 'sub',
577
+ 'such',
578
+ 'sup',
579
+ 'sure',
580
+ 't',
581
+ "t's",
582
+ 'take',
583
+ 'taken',
584
+ 'tell',
585
+ 'tends',
586
+ 'th',
587
+ 'than',
588
+ 'thank',
589
+ 'thanks',
590
+ 'thanx',
591
+ 'that',
592
+ "that's",
593
+ 'thats',
594
+ 'the',
595
+ 'their',
596
+ 'theirs',
597
+ 'them',
598
+ 'themselves',
599
+ 'then',
600
+ 'thence',
601
+ 'there',
602
+ "there's",
603
+ 'thereafter',
604
+ 'thereby',
605
+ 'therefore',
606
+ 'therein',
607
+ 'theres',
608
+ 'thereupon',
609
+ 'these',
610
+ 'they',
611
+ "they'd",
612
+ "they'll",
613
+ "they're",
614
+ "they've",
615
+ 'think',
616
+ 'third',
617
+ 'this',
618
+ 'thorough',
619
+ 'thoroughly',
620
+ 'those',
621
+ 'though',
622
+ 'three',
623
+ 'through',
624
+ 'throughout',
625
+ 'thru',
626
+ 'thus',
627
+ 'to',
628
+ 'together',
629
+ 'too',
630
+ 'took',
631
+ 'toward',
632
+ 'towards',
633
+ 'tried',
634
+ 'tries',
635
+ 'truly',
636
+ 'try',
637
+ 'trying',
638
+ 'twice',
639
+ 'two',
640
+ 'u',
641
+ 'un',
642
+ 'under',
643
+ 'unfortunately',
644
+ 'unless',
645
+ 'unlikely',
646
+ 'until',
647
+ 'unto',
648
+ 'up',
649
+ 'upon',
650
+ 'us',
651
+ 'use',
652
+ 'used',
653
+ 'useful',
654
+ 'uses',
655
+ 'using',
656
+ 'usually',
657
+ 'uucp',
658
+ 'v',
659
+ 'value',
660
+ 'various',
661
+ 'very',
662
+ 'via',
663
+ 'viz',
664
+ 'vs',
665
+ 'w',
666
+ 'want',
667
+ 'wants',
668
+ 'was',
669
+ "wasn't",
670
+ 'way',
671
+ 'we',
672
+ "we'd",
673
+ "we'll",
674
+ "we're",
675
+ "we've",
676
+ 'welcome',
677
+ 'well',
678
+ 'went',
679
+ 'were',
680
+ "weren't",
681
+ 'what',
682
+ "what's",
683
+ 'whatever',
684
+ 'when',
685
+ 'whence',
686
+ 'whenever',
687
+ 'where',
688
+ "where's",
689
+ 'whereafter',
690
+ 'whereas',
691
+ 'whereby',
692
+ 'wherein',
693
+ 'whereupon',
694
+ 'wherever',
695
+ 'whether',
696
+ 'which',
697
+ 'while',
698
+ 'whither',
699
+ 'who',
700
+ "who's",
701
+ 'whoever',
702
+ 'whole',
703
+ 'whom',
704
+ 'whose',
705
+ 'why',
706
+ 'will',
707
+ 'willing',
708
+ 'wish',
709
+ 'with',
710
+ 'within',
711
+ 'without',
712
+ "won't",
713
+ 'wonder',
714
+ 'would',
715
+ 'would',
716
+ "wouldn't",
717
+ 'x',
718
+ 'y',
719
+ 'yes',
720
+ 'yet',
721
+ 'you',
722
+ "you'd",
723
+ "you'll",
724
+ "you're",
725
+ "you've",
726
+ 'your',
727
+ 'yours',
728
+ 'yourself',
729
+ 'yourselves',
730
+ 'z',
731
+ 'zero',
732
+ ]);
733
+ /**
734
+ * Strip English stop words from a search query, preserving non-stop-word terms.
735
+ * Returns the cleaned query string. If all words are stop words, returns the original query.
736
+ */
737
+ export function stripStopWords(query) {
738
+ const terms = query.trim().split(/\s+/);
739
+ const filtered = terms.filter((t) => !ENGLISH_STOP_WORDS.has(t.toLowerCase()));
740
+ return filtered.length > 0 ? filtered.join(' ') : query;
741
+ }
742
+ export async function rebuildAllIndexes(collections) {
743
+ const elapsed = timer();
744
+ let rebuilt = 0;
745
+ const errors = [];
746
+ for (const collection of collections) {
747
+ try {
748
+ await buildFtsIndex(collection);
749
+ rebuilt++;
750
+ }
751
+ catch (err) {
752
+ errors.push(`${collection}: ${err.message}`);
753
+ }
754
+ }
755
+ emit('fts', 'rebuild', {
756
+ collections_total: collections.length,
757
+ collections_rebuilt: rebuilt,
758
+ error_count: errors.length,
759
+ duration_ms: elapsed(),
760
+ errors: errors.length > 0 ? errors : undefined,
761
+ });
762
+ }