@chainlesschain/personal-data-hub 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/adapters/browser-history-chrome.test.js +377 -0
- package/__tests__/adapters/browser-history-edge.test.js +159 -0
- package/__tests__/adapters/git-activity.test.js +216 -0
- package/__tests__/adapters/local-files.test.js +264 -0
- package/__tests__/adapters/shell-history.test.js +180 -0
- package/__tests__/adapters/system-data-android.test.js +104 -3
- package/__tests__/adapters/vscode.test.js +299 -0
- package/__tests__/adapters/win-recent.test.js +192 -0
- package/__tests__/analysis.test.js +840 -1
- package/__tests__/categories.test.js +92 -0
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +146 -0
- package/__tests__/entity-resolver-vault.test.js +5 -2
- package/__tests__/integration/local-data-adapters-pipeline.test.js +373 -0
- package/__tests__/query-parser.test.js +66 -0
- package/__tests__/registry.test.js +114 -0
- package/__tests__/sidecar-contacts-cross-validate.test.js +24 -1
- package/__tests__/sidecar-supervisor.test.js +9 -1
- package/__tests__/social-kuaishou-snapshot.test.js +55 -2
- package/__tests__/social-toutiao-snapshot.test.js +54 -2
- package/__tests__/vault-search-helpers.test.js +104 -0
- package/__tests__/vault-search.test.js +423 -0
- package/__tests__/vault.test.js +77 -3
- package/lib/adapters/browser-history-chrome/adapter.js +247 -0
- package/lib/adapters/browser-history-chrome/bookmarks-reader.js +79 -0
- package/lib/adapters/browser-history-chrome/chrome-db-reader.js +223 -0
- package/lib/adapters/browser-history-chrome/index.js +23 -0
- package/lib/adapters/browser-history-edge/adapter.js +34 -0
- package/lib/adapters/browser-history-edge/index.js +13 -0
- package/lib/adapters/git-activity/adapter.js +155 -0
- package/lib/adapters/git-activity/git-reader.js +125 -0
- package/lib/adapters/git-activity/index.js +17 -0
- package/lib/adapters/local-files/adapter.js +149 -0
- package/lib/adapters/local-files/file-walker.js +125 -0
- package/lib/adapters/local-files/index.js +18 -0
- package/lib/adapters/shell-history/adapter.js +137 -0
- package/lib/adapters/shell-history/index.js +17 -0
- package/lib/adapters/shell-history/shell-reader.js +100 -0
- package/lib/adapters/social-kuaishou/index.js +57 -1
- package/lib/adapters/social-toutiao/index.js +59 -1
- package/lib/adapters/system-data-android/adapter.js +220 -3
- package/lib/adapters/vscode/adapter.js +285 -0
- package/lib/adapters/vscode/index.js +18 -0
- package/lib/adapters/vscode/vscode-reader.js +191 -0
- package/lib/adapters/win-recent/adapter.js +150 -0
- package/lib/adapters/win-recent/index.js +16 -0
- package/lib/adapters/win-recent/win-recent-reader.js +72 -0
- package/lib/analysis.js +227 -9
- package/lib/categories.js +101 -0
- package/lib/index.js +61 -0
- package/lib/migrations.js +146 -0
- package/lib/query-parser.js +74 -0
- package/lib/registry.js +162 -0
- package/lib/vault.js +363 -2
- package/package.json +2 -1
- package/scripts/run-native-tests-sandbox.sh +53 -0
package/lib/vault.js
CHANGED
|
@@ -27,8 +27,79 @@ const fs = require("node:fs");
|
|
|
27
27
|
const path = require("node:path");
|
|
28
28
|
|
|
29
29
|
const { validate } = require("./schemas");
|
|
30
|
-
const { applyMigrations, getSchemaVersion } = require("./migrations");
|
|
30
|
+
const { applyMigrations, getSchemaVersion, getFtsMode } = require("./migrations");
|
|
31
31
|
const { isValidKeyHex } = require("./key-providers");
|
|
32
|
+
const { getCategory, PREFIX_RULES } = require("./categories");
|
|
33
|
+
|
|
34
|
+
// FTS5 trigram tokenizer requires queries of >= 3 chars to produce any
|
|
35
|
+
// trigrams at all (single 2-char input gives zero index keys → empty result).
|
|
36
|
+
// Surface this to the caller so the UI can show a hint instead of a confusing
|
|
37
|
+
// "no results" state.
|
|
38
|
+
const FTS5_MIN_QUERY_LEN = 3;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Translate a user-typed FTS5 query into a safe-to-bind string. FTS5 has its
|
|
42
|
+
* own query syntax — bare operators like `OR`, `AND`, `NOT`, `(`, `:`, `*`,
|
|
43
|
+
* double-quotes have meaning. For the browser keyword box we want literal
|
|
44
|
+
* substring search, so wrap the whole input in double quotes (FTS5 phrase
|
|
45
|
+
* mode) after escaping any embedded double quotes.
|
|
46
|
+
*/
|
|
47
|
+
function _quoteFtsQuery(q) {
|
|
48
|
+
return '"' + String(q).replace(/"/g, '""') + '"';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a (sql, params) fragment matching events for the given category.
|
|
53
|
+
* Uses categories.js PREFIX_RULES as the single source of truth so a new
|
|
54
|
+
* adapter prefix only needs to be added in one place.
|
|
55
|
+
*
|
|
56
|
+
* Returns `{ sql: "(...)", params: { catN: ... } }` or
|
|
57
|
+
* `{ sql: "0=1", params: {} }` when no rule matches (unknown category).
|
|
58
|
+
*/
|
|
59
|
+
function _categoryToWhere(category, paramPrefix = "cat") {
|
|
60
|
+
if (typeof category !== "string" || category.length === 0) {
|
|
61
|
+
return { sql: null, params: {} };
|
|
62
|
+
}
|
|
63
|
+
const matched = PREFIX_RULES.filter((rule) => rule[1] === category);
|
|
64
|
+
// "other" is a synthetic bucket — nothing in PREFIX_RULES maps to it; an
|
|
65
|
+
// event's category is "other" iff its adapter matches no prefix. Translate
|
|
66
|
+
// that to a NOT-IN-any-prefix condition.
|
|
67
|
+
if (category === "other") {
|
|
68
|
+
const exclude = [];
|
|
69
|
+
const params = {};
|
|
70
|
+
let i = 0;
|
|
71
|
+
for (const [rule] of PREFIX_RULES) {
|
|
72
|
+
const key = `${paramPrefix}${i}`;
|
|
73
|
+
if (rule.endsWith("*")) {
|
|
74
|
+
params[key] = rule.slice(0, -1) + "%";
|
|
75
|
+
exclude.push(`source_adapter NOT LIKE @${key}`);
|
|
76
|
+
} else {
|
|
77
|
+
params[key] = rule;
|
|
78
|
+
exclude.push(`source_adapter != @${key}`);
|
|
79
|
+
}
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
return { sql: "(" + exclude.join(" AND ") + ")", params };
|
|
83
|
+
}
|
|
84
|
+
if (matched.length === 0) {
|
|
85
|
+
return { sql: "0=1", params: {} };
|
|
86
|
+
}
|
|
87
|
+
const conds = [];
|
|
88
|
+
const params = {};
|
|
89
|
+
let i = 0;
|
|
90
|
+
for (const [rule] of matched) {
|
|
91
|
+
const key = `${paramPrefix}${i}`;
|
|
92
|
+
if (rule.endsWith("*")) {
|
|
93
|
+
params[key] = rule.slice(0, -1) + "%";
|
|
94
|
+
conds.push(`source_adapter LIKE @${key}`);
|
|
95
|
+
} else {
|
|
96
|
+
params[key] = rule;
|
|
97
|
+
conds.push(`source_adapter = @${key}`);
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
return { sql: "(" + conds.join(" OR ") + ")", params };
|
|
102
|
+
}
|
|
32
103
|
|
|
33
104
|
// Default SQLCipher cipher (matches WCDB / mainstream SQLCipher v4).
|
|
34
105
|
const DEFAULT_CIPHER = "sqlcipher";
|
|
@@ -277,6 +348,22 @@ class LocalVault {
|
|
|
277
348
|
source = excluded.source,
|
|
278
349
|
extra = excluded.extra,
|
|
279
350
|
ingested_at = excluded.ingested_at,
|
|
351
|
+
confidence = excluded.confidence
|
|
352
|
+
ON CONFLICT(source_adapter, source_original_id)
|
|
353
|
+
WHERE source_original_id IS NOT NULL
|
|
354
|
+
DO UPDATE SET
|
|
355
|
+
subtype = excluded.subtype,
|
|
356
|
+
occurred_at = excluded.occurred_at,
|
|
357
|
+
duration_ms = excluded.duration_ms,
|
|
358
|
+
actor = excluded.actor,
|
|
359
|
+
participants = excluded.participants,
|
|
360
|
+
place = excluded.place,
|
|
361
|
+
items = excluded.items,
|
|
362
|
+
topics = excluded.topics,
|
|
363
|
+
content = excluded.content,
|
|
364
|
+
source = excluded.source,
|
|
365
|
+
extra = excluded.extra,
|
|
366
|
+
ingested_at = excluded.ingested_at,
|
|
280
367
|
confidence = excluded.confidence`
|
|
281
368
|
)
|
|
282
369
|
.run({
|
|
@@ -325,6 +412,18 @@ class LocalVault {
|
|
|
325
412
|
source = excluded.source,
|
|
326
413
|
extra = excluded.extra,
|
|
327
414
|
ingested_at = excluded.ingested_at,
|
|
415
|
+
confidence = excluded.confidence
|
|
416
|
+
ON CONFLICT(source_adapter, source_original_id)
|
|
417
|
+
WHERE source_original_id IS NOT NULL
|
|
418
|
+
DO UPDATE SET
|
|
419
|
+
subtype = excluded.subtype,
|
|
420
|
+
names = excluded.names,
|
|
421
|
+
identifiers = excluded.identifiers,
|
|
422
|
+
relation = excluded.relation,
|
|
423
|
+
notes = excluded.notes,
|
|
424
|
+
source = excluded.source,
|
|
425
|
+
extra = excluded.extra,
|
|
426
|
+
ingested_at = excluded.ingested_at,
|
|
328
427
|
confidence = excluded.confidence`
|
|
329
428
|
)
|
|
330
429
|
.run({
|
|
@@ -370,6 +469,19 @@ class LocalVault {
|
|
|
370
469
|
source = excluded.source,
|
|
371
470
|
extra = excluded.extra,
|
|
372
471
|
ingested_at = excluded.ingested_at,
|
|
472
|
+
confidence = excluded.confidence
|
|
473
|
+
ON CONFLICT(source_adapter, source_original_id)
|
|
474
|
+
WHERE source_original_id IS NOT NULL
|
|
475
|
+
DO UPDATE SET
|
|
476
|
+
name = excluded.name,
|
|
477
|
+
coordinates_lat = excluded.coordinates_lat,
|
|
478
|
+
coordinates_lng = excluded.coordinates_lng,
|
|
479
|
+
address = excluded.address,
|
|
480
|
+
category = excluded.category,
|
|
481
|
+
aliases = excluded.aliases,
|
|
482
|
+
source = excluded.source,
|
|
483
|
+
extra = excluded.extra,
|
|
484
|
+
ingested_at = excluded.ingested_at,
|
|
373
485
|
confidence = excluded.confidence`
|
|
374
486
|
)
|
|
375
487
|
.run({
|
|
@@ -420,6 +532,21 @@ class LocalVault {
|
|
|
420
532
|
source = excluded.source,
|
|
421
533
|
extra = excluded.extra,
|
|
422
534
|
ingested_at = excluded.ingested_at,
|
|
535
|
+
confidence = excluded.confidence
|
|
536
|
+
ON CONFLICT(source_adapter, source_original_id)
|
|
537
|
+
WHERE source_original_id IS NOT NULL
|
|
538
|
+
DO UPDATE SET
|
|
539
|
+
subtype = excluded.subtype,
|
|
540
|
+
name = excluded.name,
|
|
541
|
+
category = excluded.category,
|
|
542
|
+
price_value = excluded.price_value,
|
|
543
|
+
price_currency = excluded.price_currency,
|
|
544
|
+
merchant = excluded.merchant,
|
|
545
|
+
external_url = excluded.external_url,
|
|
546
|
+
thumbnail_local_path = excluded.thumbnail_local_path,
|
|
547
|
+
source = excluded.source,
|
|
548
|
+
extra = excluded.extra,
|
|
549
|
+
ingested_at = excluded.ingested_at,
|
|
423
550
|
confidence = excluded.confidence`
|
|
424
551
|
)
|
|
425
552
|
.run({
|
|
@@ -546,6 +673,50 @@ class LocalVault {
|
|
|
546
673
|
.run(adapter, originalId, capturedAt, json);
|
|
547
674
|
}
|
|
548
675
|
|
|
676
|
+
/**
|
|
677
|
+
* 2026-05-24 — iterate raw_events sequentially for re-derive flow.
|
|
678
|
+
* Returns rows shaped like the original raw payload object the adapter
|
|
679
|
+
* yielded ({ originalId, capturedAt, payload }) so the caller can feed
|
|
680
|
+
* directly into adapter.normalize().
|
|
681
|
+
*
|
|
682
|
+
* @param {object} [opts]
|
|
683
|
+
* @param {string} [opts.adapter] Filter by adapter name; default = all
|
|
684
|
+
* @param {number} [opts.limit] Max rows; default = unlimited
|
|
685
|
+
* @param {number} [opts.offset=0] Skip first N rows
|
|
686
|
+
* @returns {Array<{adapter: string, originalId: string, capturedAt: number, payload: object}>}
|
|
687
|
+
*/
|
|
688
|
+
queryRawEvents({ adapter, limit, offset = 0 } = {}) {
|
|
689
|
+
const db = this._requireOpen();
|
|
690
|
+
let sql =
|
|
691
|
+
"SELECT adapter, original_id, captured_at, payload FROM raw_events";
|
|
692
|
+
const args = [];
|
|
693
|
+
if (adapter) {
|
|
694
|
+
sql += " WHERE adapter = ?";
|
|
695
|
+
args.push(adapter);
|
|
696
|
+
}
|
|
697
|
+
sql += " ORDER BY adapter, captured_at, original_id";
|
|
698
|
+
if (Number.isInteger(limit) && limit > 0) {
|
|
699
|
+
sql += " LIMIT ? OFFSET ?";
|
|
700
|
+
args.push(limit, Number.isInteger(offset) ? offset : 0);
|
|
701
|
+
} else if (Number.isInteger(offset) && offset > 0) {
|
|
702
|
+
sql += " LIMIT -1 OFFSET ?";
|
|
703
|
+
args.push(offset);
|
|
704
|
+
}
|
|
705
|
+
const rows = db.prepare(sql).all(...args);
|
|
706
|
+
return rows.map((r) => ({
|
|
707
|
+
adapter: r.adapter,
|
|
708
|
+
originalId: r.original_id,
|
|
709
|
+
capturedAt: r.captured_at,
|
|
710
|
+
payload: (() => {
|
|
711
|
+
try {
|
|
712
|
+
return JSON.parse(r.payload);
|
|
713
|
+
} catch (_e) {
|
|
714
|
+
return r.payload; // raw string if not JSON
|
|
715
|
+
}
|
|
716
|
+
})(),
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
|
|
549
720
|
// ─── Entity reads ──────────────────────────────────────────────────────
|
|
550
721
|
|
|
551
722
|
getEvent(id) {
|
|
@@ -734,6 +905,190 @@ class LocalVault {
|
|
|
734
905
|
.map((row) => this._rowToItem(row));
|
|
735
906
|
}
|
|
736
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Mode (`'fts5'` or `'like'`) recorded by migration 3. Determines whether
|
|
910
|
+
* searchEvents uses the FTS5 virtual table or falls back to LIKE scans.
|
|
911
|
+
* Cached on first read.
|
|
912
|
+
*/
|
|
913
|
+
ftsMode() {
|
|
914
|
+
if (!this._ftsMode) this._ftsMode = getFtsMode(this._requireOpen());
|
|
915
|
+
return this._ftsMode;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Full-text + faceted search over events for the Vault Browser UI.
|
|
920
|
+
*
|
|
921
|
+
* Filters (all optional, ANDed):
|
|
922
|
+
* q — keyword string; FTS5 phrase match if length >= 3 (or LIKE
|
|
923
|
+
* substring match on subtype/content/actor/place/extra in
|
|
924
|
+
* fallback mode). Shorter queries are ignored as if absent.
|
|
925
|
+
* adapter — exact source_adapter
|
|
926
|
+
* category — one of categories.CATEGORIES; expands to adapter prefix list
|
|
927
|
+
* subtype — exact subtype match
|
|
928
|
+
* since — occurred_at >= since (ms epoch)
|
|
929
|
+
* until — occurred_at <= until
|
|
930
|
+
* cursor — { occurredAt, id } from previous page's last row
|
|
931
|
+
* limit — default 50, max 500
|
|
932
|
+
*
|
|
933
|
+
* Pagination is cursor-based on (occurred_at DESC, id DESC) — stable under
|
|
934
|
+
* concurrent inserts (newer events appear only on re-fetch of page 1).
|
|
935
|
+
*
|
|
936
|
+
* Returns `{ rows: Event[], nextCursor: {occurredAt, id} | null, mode: 'fts5'|'like', shortQuery: boolean }`.
|
|
937
|
+
* - shortQuery=true means the q was non-empty but below FTS5_MIN_QUERY_LEN
|
|
938
|
+
* and was dropped — UI should hint "请输入至少 3 个字".
|
|
939
|
+
*/
|
|
940
|
+
searchEvents(q = {}) {
|
|
941
|
+
const db = this._requireOpen();
|
|
942
|
+
const mode = this.ftsMode();
|
|
943
|
+
const limit = Number.isInteger(q.limit) && q.limit > 0 ? Math.min(q.limit, 500) : 50;
|
|
944
|
+
|
|
945
|
+
const where = [];
|
|
946
|
+
const params = { limit: limit + 1 }; // +1 to detect "is there a next page?"
|
|
947
|
+
|
|
948
|
+
let shortQuery = false;
|
|
949
|
+
const rawQ = typeof q.q === "string" ? q.q.trim() : "";
|
|
950
|
+
|
|
951
|
+
// Keyword filter — FTS5 path uses MATCH on events_fts; LIKE path does
|
|
952
|
+
// OR across the 5 indexed columns. Sub-min-length q in FTS5 mode is
|
|
953
|
+
// dropped silently (and reported back as shortQuery=true).
|
|
954
|
+
let joinFts = false;
|
|
955
|
+
if (rawQ.length > 0) {
|
|
956
|
+
if (mode === "fts5") {
|
|
957
|
+
if (rawQ.length >= FTS5_MIN_QUERY_LEN) {
|
|
958
|
+
joinFts = true;
|
|
959
|
+
params.q = _quoteFtsQuery(rawQ);
|
|
960
|
+
where.push("events_fts MATCH @q");
|
|
961
|
+
} else {
|
|
962
|
+
shortQuery = true;
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
params.qLike = "%" + rawQ + "%";
|
|
966
|
+
where.push(
|
|
967
|
+
"(subtype LIKE @qLike OR content LIKE @qLike OR actor LIKE @qLike OR place LIKE @qLike OR extra LIKE @qLike)"
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (q.adapter) {
|
|
973
|
+
where.push("source_adapter = @adapter");
|
|
974
|
+
params.adapter = q.adapter;
|
|
975
|
+
}
|
|
976
|
+
if (q.category) {
|
|
977
|
+
const { sql, params: catParams } = _categoryToWhere(q.category);
|
|
978
|
+
if (sql) {
|
|
979
|
+
where.push(sql);
|
|
980
|
+
Object.assign(params, catParams);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (q.subtype) {
|
|
984
|
+
where.push("e.subtype = @subtype");
|
|
985
|
+
params.subtype = q.subtype;
|
|
986
|
+
}
|
|
987
|
+
if (Number.isFinite(q.since)) {
|
|
988
|
+
where.push("occurred_at >= @since");
|
|
989
|
+
params.since = q.since;
|
|
990
|
+
}
|
|
991
|
+
if (Number.isFinite(q.until)) {
|
|
992
|
+
where.push("occurred_at <= @until");
|
|
993
|
+
params.until = q.until;
|
|
994
|
+
}
|
|
995
|
+
// Cursor: rows strictly older than the cursor's (occurred_at, id) tuple.
|
|
996
|
+
// SQLite tuple comparison handles this natively.
|
|
997
|
+
if (q.cursor && Number.isFinite(q.cursor.occurredAt) && typeof q.cursor.id === "string") {
|
|
998
|
+
where.push("(occurred_at, e.id) < (@cursorAt, @cursorId)");
|
|
999
|
+
params.cursorAt = q.cursor.occurredAt;
|
|
1000
|
+
params.cursorId = q.cursor.id;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const sql =
|
|
1004
|
+
"SELECT e.* FROM events e" +
|
|
1005
|
+
(joinFts ? " JOIN events_fts f ON e.rowid = f.rowid" : "") +
|
|
1006
|
+
(where.length ? " WHERE " + where.join(" AND ") : "") +
|
|
1007
|
+
" ORDER BY occurred_at DESC, e.id DESC LIMIT @limit";
|
|
1008
|
+
|
|
1009
|
+
const rowsPlusOne = db.prepare(sql).all(params);
|
|
1010
|
+
const hasMore = rowsPlusOne.length > limit;
|
|
1011
|
+
const rows = hasMore ? rowsPlusOne.slice(0, limit) : rowsPlusOne;
|
|
1012
|
+
const events = rows.map((r) => this._rowToEvent(r));
|
|
1013
|
+
const last = rows[rows.length - 1];
|
|
1014
|
+
return {
|
|
1015
|
+
rows: events,
|
|
1016
|
+
nextCursor: hasMore && last ? { occurredAt: last.occurred_at, id: last.id } : null,
|
|
1017
|
+
mode,
|
|
1018
|
+
shortQuery,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Counts events grouped by category / adapter / subtype, honoring the
|
|
1024
|
+
* same q/since/until filters as searchEvents. Powers the sidebar badges
|
|
1025
|
+
* + adapter chips ("社交聊天 (123)" / "social-bilibili (45)").
|
|
1026
|
+
*
|
|
1027
|
+
* Returns `{ byCategory, byAdapter, bySubtype, total, mode, shortQuery }`.
|
|
1028
|
+
* Each map is `{ key: count }`. Empty keys are omitted.
|
|
1029
|
+
*/
|
|
1030
|
+
facetCounts(q = {}) {
|
|
1031
|
+
const db = this._requireOpen();
|
|
1032
|
+
const mode = this.ftsMode();
|
|
1033
|
+
const params = {};
|
|
1034
|
+
const where = [];
|
|
1035
|
+
|
|
1036
|
+
let shortQuery = false;
|
|
1037
|
+
const rawQ = typeof q.q === "string" ? q.q.trim() : "";
|
|
1038
|
+
let joinFts = false;
|
|
1039
|
+
if (rawQ.length > 0) {
|
|
1040
|
+
if (mode === "fts5") {
|
|
1041
|
+
if (rawQ.length >= FTS5_MIN_QUERY_LEN) {
|
|
1042
|
+
joinFts = true;
|
|
1043
|
+
params.q = _quoteFtsQuery(rawQ);
|
|
1044
|
+
where.push("events_fts MATCH @q");
|
|
1045
|
+
} else {
|
|
1046
|
+
shortQuery = true;
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
params.qLike = "%" + rawQ + "%";
|
|
1050
|
+
where.push(
|
|
1051
|
+
"(subtype LIKE @qLike OR content LIKE @qLike OR actor LIKE @qLike OR place LIKE @qLike OR extra LIKE @qLike)"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (Number.isFinite(q.since)) {
|
|
1056
|
+
where.push("occurred_at >= @since");
|
|
1057
|
+
params.since = q.since;
|
|
1058
|
+
}
|
|
1059
|
+
if (Number.isFinite(q.until)) {
|
|
1060
|
+
where.push("occurred_at <= @until");
|
|
1061
|
+
params.until = q.until;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const baseFrom =
|
|
1065
|
+
"FROM events e" + (joinFts ? " JOIN events_fts f ON e.rowid = f.rowid" : "");
|
|
1066
|
+
const whereSql = where.length ? " WHERE " + where.join(" AND ") : "";
|
|
1067
|
+
|
|
1068
|
+
const adapterRows = db
|
|
1069
|
+
.prepare(
|
|
1070
|
+
`SELECT source_adapter AS k, COUNT(*) AS n ${baseFrom}${whereSql} GROUP BY source_adapter`
|
|
1071
|
+
)
|
|
1072
|
+
.all(params);
|
|
1073
|
+
const subtypeRows = db
|
|
1074
|
+
.prepare(`SELECT e.subtype AS k, COUNT(*) AS n ${baseFrom}${whereSql} GROUP BY e.subtype`)
|
|
1075
|
+
.all(params);
|
|
1076
|
+
|
|
1077
|
+
const byAdapter = {};
|
|
1078
|
+
const byCategory = {};
|
|
1079
|
+
let total = 0;
|
|
1080
|
+
for (const r of adapterRows) {
|
|
1081
|
+
byAdapter[r.k] = r.n;
|
|
1082
|
+
const cat = getCategory(r.k);
|
|
1083
|
+
byCategory[cat] = (byCategory[cat] || 0) + r.n;
|
|
1084
|
+
total += r.n;
|
|
1085
|
+
}
|
|
1086
|
+
const bySubtype = {};
|
|
1087
|
+
for (const r of subtypeRows) bySubtype[r.k] = r.n;
|
|
1088
|
+
|
|
1089
|
+
return { byCategory, byAdapter, bySubtype, total, mode, shortQuery };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
737
1092
|
countEvents(q = {}) {
|
|
738
1093
|
const where = [];
|
|
739
1094
|
const params = {};
|
|
@@ -1275,4 +1630,10 @@ class LocalVault {
|
|
|
1275
1630
|
}
|
|
1276
1631
|
}
|
|
1277
1632
|
|
|
1278
|
-
module.exports = {
|
|
1633
|
+
module.exports = {
|
|
1634
|
+
LocalVault,
|
|
1635
|
+
_internal: { loadDriver, formatDriverLoadError },
|
|
1636
|
+
// Pure-JS helpers exported for unit testing without the native bs3mc
|
|
1637
|
+
// binding (search SQL builders, category WHERE translator, FTS5 escape).
|
|
1638
|
+
_searchHelpers: { _categoryToWhere, _quoteFtsQuery, FTS5_MIN_QUERY_LEN },
|
|
1639
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainlesschain/personal-data-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Personal Data Hub — UnifiedSchema + validators + KG ingest helpers for the data-back-to-the-individual middleware",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./migrations": "./lib/migrations.js",
|
|
15
15
|
"./key-providers": "./lib/key-providers.js",
|
|
16
16
|
"./adapter-spec": "./lib/adapter-spec.js",
|
|
17
|
+
"./categories": "./lib/categories.js",
|
|
17
18
|
"./kg-derive": "./lib/kg-derive.js",
|
|
18
19
|
"./rag-derive": "./lib/rag-derive.js",
|
|
19
20
|
"./registry": "./lib/registry.js",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run vault-search.test.js (FTS5 native integration) in an isolated sandbox.
|
|
3
|
+
#
|
|
4
|
+
# Background: the root node_modules bs3mc binding is built for Electron 39
|
|
5
|
+
# (NODE_MODULE_VERSION 140) which doesn't match ANY Node.js version
|
|
6
|
+
# (Node 24 = ABI 137, Node 25 = ABI 141). Plain `npm test` fails locally with
|
|
7
|
+
# ABI mismatch. CI is unaffected because its node_modules is built fresh.
|
|
8
|
+
#
|
|
9
|
+
# This script:
|
|
10
|
+
# 1. Copies lib/ + the target test into a temp sandbox
|
|
11
|
+
# 2. Installs a separate bs3mc compiled for the CURRENT host Node ABI
|
|
12
|
+
# 3. Runs vitest against it
|
|
13
|
+
#
|
|
14
|
+
# Idempotent; rerun any time. Sandbox lives under $TMPDIR/pdh-fts5-sandbox
|
|
15
|
+
# so it survives between runs (faster re-install).
|
|
16
|
+
#
|
|
17
|
+
# Usage: bash packages/personal-data-hub/scripts/run-native-tests-sandbox.sh
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
22
|
+
SANDBOX="${TMPDIR:-/tmp}/pdh-fts5-sandbox"
|
|
23
|
+
|
|
24
|
+
echo "==> Sandbox: $SANDBOX"
|
|
25
|
+
mkdir -p "$SANDBOX/lib" "$SANDBOX/__tests__"
|
|
26
|
+
|
|
27
|
+
# Sync sources every run (lib/ may have evolved since last sandbox build)
|
|
28
|
+
cp -r "$ROOT/lib/." "$SANDBOX/lib/"
|
|
29
|
+
cp "$ROOT/__tests__/vault-search.test.js" "$SANDBOX/__tests__/"
|
|
30
|
+
|
|
31
|
+
# Minimal package.json — only the deps the target test needs.
|
|
32
|
+
cat > "$SANDBOX/package.json" <<'EOF'
|
|
33
|
+
{
|
|
34
|
+
"name": "pdh-fts5-sandbox",
|
|
35
|
+
"version": "0.0.0",
|
|
36
|
+
"private": true,
|
|
37
|
+
"type": "commonjs",
|
|
38
|
+
"scripts": { "test": "vitest run" },
|
|
39
|
+
"dependencies": { "better-sqlite3-multiple-ciphers": "^12.5.0" },
|
|
40
|
+
"devDependencies": { "vitest": "^4.1.5" }
|
|
41
|
+
}
|
|
42
|
+
EOF
|
|
43
|
+
|
|
44
|
+
if [ ! -d "$SANDBOX/node_modules/better-sqlite3-multiple-ciphers/build" ]; then
|
|
45
|
+
echo "==> Installing deps (one-time, ~30-60s)"
|
|
46
|
+
(cd "$SANDBOX" && npm install --no-audit --no-fund --loglevel=warn)
|
|
47
|
+
else
|
|
48
|
+
echo "==> Deps already installed (skipping npm install)"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "==> Running tests"
|
|
52
|
+
cd "$SANDBOX"
|
|
53
|
+
exec node ./node_modules/vitest/vitest.mjs run
|