@kybernesis/brain-storage-sqlite 0.8.4 → 0.10.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 (56) hide show
  1. package/dist/hash.d.ts +9 -0
  2. package/dist/hash.d.ts.map +1 -0
  3. package/dist/hash.js +12 -0
  4. package/dist/hash.js.map +1 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +32 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/migrations/backup.d.ts +14 -0
  10. package/dist/migrations/backup.d.ts.map +1 -0
  11. package/dist/migrations/backup.js +20 -0
  12. package/dist/migrations/backup.js.map +1 -0
  13. package/dist/migrations/config.d.ts +14 -0
  14. package/dist/migrations/config.d.ts.map +1 -0
  15. package/dist/migrations/config.js +24 -0
  16. package/dist/migrations/config.js.map +1 -0
  17. package/dist/migrations/errors.d.ts +24 -0
  18. package/dist/migrations/errors.d.ts.map +1 -0
  19. package/dist/migrations/errors.js +45 -0
  20. package/dist/migrations/errors.js.map +1 -0
  21. package/dist/migrations/registry.d.ts +15 -0
  22. package/dist/migrations/registry.d.ts.map +1 -0
  23. package/dist/migrations/registry.js +34 -0
  24. package/dist/migrations/registry.js.map +1 -0
  25. package/dist/migrations/runner.d.ts +57 -0
  26. package/dist/migrations/runner.d.ts.map +1 -0
  27. package/dist/migrations/runner.js +141 -0
  28. package/dist/migrations/runner.js.map +1 -0
  29. package/dist/migrations/timeline/002-file-provenance.d.ts +24 -0
  30. package/dist/migrations/timeline/002-file-provenance.d.ts.map +1 -0
  31. package/dist/migrations/timeline/002-file-provenance.js +43 -0
  32. package/dist/migrations/timeline/002-file-provenance.js.map +1 -0
  33. package/dist/migrations/timeline/002-proof.d.ts +13 -0
  34. package/dist/migrations/timeline/002-proof.d.ts.map +1 -0
  35. package/dist/migrations/timeline/002-proof.js +16 -0
  36. package/dist/migrations/timeline/002-proof.js.map +1 -0
  37. package/dist/migrations/types.d.ts +52 -0
  38. package/dist/migrations/types.d.ts.map +1 -0
  39. package/dist/migrations/types.js +15 -0
  40. package/dist/migrations/types.js.map +1 -0
  41. package/dist/provider.d.ts +13 -3
  42. package/dist/provider.d.ts.map +1 -1
  43. package/dist/provider.js +9 -57
  44. package/dist/provider.js.map +1 -1
  45. package/dist/reconcile.d.ts +76 -0
  46. package/dist/reconcile.d.ts.map +1 -0
  47. package/dist/reconcile.js +120 -0
  48. package/dist/reconcile.js.map +1 -0
  49. package/dist/timeline.d.ts.map +1 -1
  50. package/dist/timeline.js +67 -10
  51. package/dist/timeline.js.map +1 -1
  52. package/dist/vec-lazy.d.ts +19 -0
  53. package/dist/vec-lazy.d.ts.map +1 -0
  54. package/dist/vec-lazy.js +76 -0
  55. package/dist/vec-lazy.js.map +1 -0
  56. package/package.json +5 -4
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Spec B (docs/specs/file-provenance-and-reconcile.md): file provenance.
3
+ *
4
+ * Adds the mutable LOCATOR (`file_path`, relative path, type='file' rows only)
5
+ * and the VERSION fingerprint (`content_hash` = sha256 of raw file bytes) to
6
+ * timeline_events. Stable identity (random source_path / chunk origin_id) is
7
+ * deliberately untouched — separating identity from locator is what makes a
8
+ * rename a one-row UPDATE.
9
+ *
10
+ * The unique index is PARTIAL: conversation rows keep file_path NULL in any
11
+ * number; non-NULL paths are unique (the dual-key upsert rides on it — proven
12
+ * in partial-upsert.spike.test.ts on better-sqlite3 + libsql).
13
+ *
14
+ * Backfill stamps file_path from the legacy `title` ("… File: <relpath>") —
15
+ * in-DB only, no file reads; content_hash stays NULL on legacy rows (the first
16
+ * reconcile run stamps it lazily). DEDUPE-AWARE: legacy brains can hold
17
+ * duplicate rows for the same file (the pile-up bug Spec B fixes) — stamping
18
+ * all of them would violate the new unique index and roll back the whole
19
+ * migration, so only the NEWEST row (MAX(id)) per title gets the locator;
20
+ * older duplicates stay NULL and remain reachable by source_path.
21
+ */
22
+ export const migration = {
23
+ id: '002-file-provenance',
24
+ db: 'timeline',
25
+ statements: [
26
+ `ALTER TABLE timeline_events ADD COLUMN file_path TEXT`,
27
+ `ALTER TABLE timeline_events ADD COLUMN content_hash TEXT`,
28
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_timeline_file_path
29
+ ON timeline_events(file_path) WHERE file_path IS NOT NULL`,
30
+ ],
31
+ backfill(db) {
32
+ db.exec(`
33
+ UPDATE timeline_events
34
+ SET file_path = substr(title, instr(title, 'File: ') + 6)
35
+ WHERE id IN (
36
+ SELECT MAX(id) FROM timeline_events
37
+ WHERE type = 'file' AND file_path IS NULL AND title LIKE '%File: %'
38
+ GROUP BY title
39
+ )
40
+ `);
41
+ },
42
+ };
43
+ //# sourceMappingURL=002-file-provenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"002-file-provenance.js","sourceRoot":"","sources":["../../../src/migrations/timeline/002-file-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,CAAC,MAAM,SAAS,GAAc;IAClC,EAAE,EAAE,qBAAqB;IACzB,EAAE,EAAE,UAAU;IACd,UAAU,EAAE;QACV,uDAAuD;QACvD,0DAA0D;QAC1D;iEAC6D;KAC9D;IACD,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,IAAI,CAAC;;;;;;;;KAQP,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Proof migration (Spec A T8 / C9): a trivial additive column purely to
3
+ * exercise the runner end-to-end against a copy of a real brain.
4
+ *
5
+ * Deliberately NOT in the default registry — shipping a probe column to every
6
+ * consumer has no value. The real-brain regression test injects it via
7
+ * `opts.registry`; the first SHIPPED migration will be Spec B's provenance
8
+ * columns (which must also update the matching ensure*Schema CREATE body —
9
+ * see registry.ts).
10
+ */
11
+ import type { Migration } from '../types.js';
12
+ export declare const proofMigration: Migration;
13
+ //# sourceMappingURL=002-proof.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"002-proof.d.ts","sourceRoot":"","sources":["../../../src/migrations/timeline/002-proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,eAAO,MAAM,cAAc,EAAE,SAI5B,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Proof migration (Spec A T8 / C9): a trivial additive column purely to
3
+ * exercise the runner end-to-end against a copy of a real brain.
4
+ *
5
+ * Deliberately NOT in the default registry — shipping a probe column to every
6
+ * consumer has no value. The real-brain regression test injects it via
7
+ * `opts.registry`; the first SHIPPED migration will be Spec B's provenance
8
+ * columns (which must also update the matching ensure*Schema CREATE body —
9
+ * see registry.ts).
10
+ */
11
+ export const proofMigration = {
12
+ id: '002-proof',
13
+ db: 'timeline',
14
+ statements: ['ALTER TABLE timeline_events ADD COLUMN _migration_probe TEXT'],
15
+ };
16
+ //# sourceMappingURL=002-proof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"002-proof.js","sourceRoot":"","sources":["../../../src/migrations/timeline/002-proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,EAAE,EAAE,WAAW;IACf,EAAE,EAAE,UAAU;IACd,UAAU,EAAE,CAAC,8DAA8D,CAAC;CAC7E,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Schema migration types (Spec A — docs/specs/migration-capability.md).
3
+ *
4
+ * A migration is a small, named, immutable-once-shipped module. Its DDL is
5
+ * DECLARATIVE (`statements`), not an imperative `up(db)` — the runner executes
6
+ * the statements in order. Declarative SQL keeps the checksum honest (we hash
7
+ * the normalized SQL, not JS source) and stays dialect-portable for a future
8
+ * libsql/Turso backend (Arcana).
9
+ *
10
+ * MVP constraint: statements must be ADDITIVE (`ALTER TABLE ... ADD COLUMN`,
11
+ * `CREATE TABLE/INDEX IF NOT EXISTS`) — no drop/rewrite. Table-rebuild
12
+ * migrations are out of MVP scope.
13
+ */
14
+ import type Database from 'better-sqlite3';
15
+ /**
16
+ * The four brain DBs that carry migrations. Subset of `DbKind` in index.ts
17
+ * (same spelling — `entityGraph`, not `entity-graph`); `idempotency` is a
18
+ * cache-shaped DB with no migration line.
19
+ */
20
+ export type MigrationKind = 'timeline' | 'entityGraph' | 'sleep' | 'vectors';
21
+ export interface Migration {
22
+ /** Ordered + named, e.g. '002-add-file-provenance'. NEVER edit after release (checksum guards it). */
23
+ readonly id: string;
24
+ /** Which DB file this migration belongs to (each carries its own _migrations table). */
25
+ readonly db: MigrationKind;
26
+ /** Plain-SQLite DDL, executed in order inside the migration transaction. Checksummed. */
27
+ readonly statements: readonly string[];
28
+ /**
29
+ * Marks the no-op baseline marker (one per kind, by convention `001-baseline`).
30
+ * On a LEGACY unversioned DB (sentinel table present, no `_migrations`), only
31
+ * baseline-flagged entries are stamped as applied without running.
32
+ */
33
+ readonly baseline?: boolean;
34
+ /** Optional data backfill — idempotent, resumable; runs after `statements` in the same txn. */
35
+ backfill?(db: Database.Database): void;
36
+ }
37
+ export interface MigrationOptions {
38
+ /**
39
+ * 'apply' — advance schema (local consumers: KBDE / KyberBot via autoMigrate;
40
+ * Arcana's migrator job per-tenant).
41
+ * 'check' — never write; throw SchemaBehindError on pending (Arcana serving path).
42
+ * Refuse-when-ahead applies in BOTH modes.
43
+ */
44
+ mode: 'apply' | 'check';
45
+ /**
46
+ * Invoked once before the first pending migration applies (apply mode only).
47
+ * Local: copyFileBackup (WAL checkpoint + `<name>.pre-<firstPendingId>.bak`).
48
+ * Cloud: snapshot hook or no-op.
49
+ */
50
+ backup?: (db: Database.Database, firstPendingId: string) => void;
51
+ }
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,SAAS;IACxB,sGAAsG;IACtG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,+FAA+F;IAC/F,QAAQ,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Schema migration types (Spec A — docs/specs/migration-capability.md).
3
+ *
4
+ * A migration is a small, named, immutable-once-shipped module. Its DDL is
5
+ * DECLARATIVE (`statements`), not an imperative `up(db)` — the runner executes
6
+ * the statements in order. Declarative SQL keeps the checksum honest (we hash
7
+ * the normalized SQL, not JS source) and stays dialect-portable for a future
8
+ * libsql/Turso backend (Arcana).
9
+ *
10
+ * MVP constraint: statements must be ADDITIVE (`ALTER TABLE ... ADD COLUMN`,
11
+ * `CREATE TABLE/INDEX IF NOT EXISTS`) — no drop/rewrite. Table-rebuild
12
+ * migrations are out of MVP scope.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -10,7 +10,17 @@
10
10
  * corresponding brain-core module; behaviour is held constant.
11
11
  */
12
12
  import type { StorageProvider } from '@kybernesis/brain-contracts';
13
- /** Reset the lazy vec-store cache (test/utility). */
14
- export declare function resetLazyVectorStore(): void;
15
- export declare function createSqliteStorageProvider(): StorageProvider;
13
+ import type { MigrationOptions } from './migrations/types.js';
14
+ export { resetLazyVectorStore } from './vec-lazy.js';
15
+ export interface SqliteStorageProviderOptions {
16
+ /**
17
+ * Migration trigger (Spec A) — consumer-chosen, off when omitted.
18
+ * `true` → local default `{ mode:'apply', backup: copyFileBackup }` (KBDE /
19
+ * KyberBot: self-heal on open). Cloud serving (Arcana) passes
20
+ * `{ mode:'check' }` — refuse ahead AND behind, never write; its migrator
21
+ * job calls `runMigrations` standalone per tenant.
22
+ */
23
+ autoMigrate?: boolean | MigrationOptions;
24
+ }
25
+ export declare function createSqliteStorageProvider(options?: SqliteStorageProviderOptions): StorageProvider;
16
26
  //# sourceMappingURL=provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,eAAe,EAIhB,MAAM,6BAA6B,CAAC;AAgCrC,qDAAqD;AACrD,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C;AA6BD,wBAAgB,2BAA2B,IAAI,eAAe,CAiB7D"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAa9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;CAC1C;AAED,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,4BAAiC,GACzC,eAAe,CAoBjB"}
package/dist/provider.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * The SQL that lands here is moved verbatim (port-faithful) from the
10
10
  * corresponding brain-core module; behaviour is held constant.
11
11
  */
12
+ import { setAutoMigrate } from './migrations/config.js';
12
13
  import { sqliteFactStore } from './fact-store.js';
13
14
  import { sqliteTimelineStore } from './timeline.js';
14
15
  import { sqliteEntityGraphStore } from './entity-graph.js';
@@ -16,64 +17,15 @@ import { sqliteSleepStore } from './sleep-store.js';
16
17
  import { sqliteSleepMaintenance } from './sleep-maintenance.js';
17
18
  import { sqliteSearchQueries } from './search-queries.js';
18
19
  import { sqliteFactRetrievalQueries } from './fact-retrieval-queries.js';
19
- // ─── Lazy vector store ────────────────────────────────────────────────────────
20
- // VectorRepository is a first-class member of the bundle (decision #3 Option A), but
21
- // brain-storage-vec carries the sqlite-vec NATIVE binary. We load it lazily on
22
- // first vector use so it never costs apps anything at provider-creation time,
23
- // and brain-storage-sqlite keeps NO static dependency on sqlite-vec. If the
24
- // package can't be loaded (not installed / wrong platform), the store degrades
25
- // to safe no-ops — brain-core's vectors layer maps these to its graceful results.
26
- let _vecStore = null;
27
- let _vecAttempted = false;
28
- async function loadVecStore() {
29
- if (_vecAttempted)
30
- return _vecStore;
31
- _vecAttempted = true;
32
- try {
33
- const mod = await import('@kybernesis/brain-storage-vec');
34
- _vecStore = mod.sqliteVecStore;
20
+ // Lazy vector store lives in vec-lazy.ts (Spec B: timeline.ts also needs it
21
+ // for cluster cleanup, and importing provider.ts from timeline.ts would close
22
+ // the documented eval-order cycle). Re-exported here for API compatibility.
23
+ import { lazyVectorStore } from './vec-lazy.js';
24
+ export { resetLazyVectorStore } from './vec-lazy.js';
25
+ export function createSqliteStorageProvider(options = {}) {
26
+ if (options.autoMigrate !== undefined) {
27
+ setAutoMigrate(options.autoMigrate);
35
28
  }
36
- catch {
37
- _vecStore = null;
38
- }
39
- return _vecStore;
40
- }
41
- /** Reset the lazy vec-store cache (test/utility). */
42
- export function resetLazyVectorStore() {
43
- _vecStore = null;
44
- _vecAttempted = false;
45
- }
46
- const lazyVectorStore = {
47
- async available(t) {
48
- const s = await loadVecStore();
49
- return s ? s.available(t) : false;
50
- },
51
- async hasOrigin(t, originId) {
52
- const s = await loadVecStore();
53
- if (!s)
54
- return false;
55
- return s.hasOrigin(t, originId);
56
- },
57
- async insertChunk(t, chunk) {
58
- const s = await loadVecStore();
59
- if (!s)
60
- throw new Error('[brain-storage-sqlite] vector backend unavailable');
61
- return s.insertChunk(t, chunk);
62
- },
63
- async search(t, embedding, limit) {
64
- const s = await loadVecStore();
65
- if (!s)
66
- return [];
67
- return s.search(t, embedding, limit);
68
- },
69
- async stats(t) {
70
- const s = await loadVecStore();
71
- if (!s)
72
- return { count: 0, available: false };
73
- return s.stats(t);
74
- },
75
- };
76
- export function createSqliteStorageProvider() {
77
29
  return {
78
30
  repositories: {
79
31
  timeline: sqliteTimelineStore,
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAEzE,4EAA4E;AAC5E,8EAA8E;AAC9E,4EAA4E;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAarD,MAAM,UAAU,2BAA2B,CACzC,UAAwC,EAAE;IAE1C,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,cAAc,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACtC,CAAC;IACD,OAAO;QACL,YAAY,EAAE;YACZ,QAAQ,EAAE,mBAAmB;YAC7B,QAAQ,EAAE,sBAAsB;YAChC,KAAK,EAAE,eAAe;YACtB,KAAK,EAAE,gBAAgB;YACvB,OAAO,EAAE,eAAe;SACzB;QACD,gFAAgF;QAChF,MAAM,EAAE;YACN,MAAM,EAAE,mBAAmB,EAAE,KAAK;YAClC,aAAa,EAAE,0BAA0B,EAAE,KAAK;YAChD,KAAK,EAAE,sBAAsB,EAAE,OAAO;SAEvC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Reconcile (Spec B) — diff the brain against the filesystem and converge.
3
+ * This is the capability that catches the 42-files class: files marked
4
+ * "synced" but silently absent from the brain.
5
+ *
6
+ * REPORT-ONLY by default: a plain run returns structured findings and makes no
7
+ * destructive change. The one write a report run performs is the documented
8
+ * lazy baseline (resolved decision #2): a legacy row with NULL content_hash
9
+ * whose file exists on disk gets the CURRENT file's hash stamped and is
10
+ * classified up-to-date — trusting the present file as baseline avoids a full
11
+ * re-embed of every legacy row.
12
+ *
13
+ * Classification:
14
+ * missing — on disk, no brain row (→ repair: re-ingest via callback)
15
+ * stale — both, hashes differ (→ repair: re-ingest via callback)
16
+ * orphaned — brain row, no file on disk (→ removeOrphaned: delete cluster)
17
+ * renamed — a missing path whose hash equals an orphaned row's hash
18
+ * (→ repair: ONE file_path UPDATE — cluster, enrichment, and
19
+ * embeddings all survive; no re-ingest, no re-embed)
20
+ *
21
+ * Rename-and-edit is NOT detected (hashes differ) — it degrades to the
22
+ * missing + orphaned pair, i.e. add + remove. Documented limitation.
23
+ *
24
+ * Re-ingest is delegated to `opts.reingest` because ingestion is brain-core's
25
+ * pipeline (LLM extraction + embedding) and the dependency points the other
26
+ * way. The consumer (KBDE/KyberBot sweep, Arcana job) wires storeConversation
27
+ * into it — see the spec's consumer-integration section.
28
+ *
29
+ * Filesystem truth = the walked roots; the watched-folders manifest is never
30
+ * consulted. Dotfiles and dot-directories are skipped.
31
+ */
32
+ import type { TenantContext } from '@kybernesis/brain-contracts';
33
+ export interface ReconcileDiskFile {
34
+ file_path: string;
35
+ absolute_path: string;
36
+ content_hash: string;
37
+ }
38
+ export interface ReconcileReport {
39
+ missing: ReconcileDiskFile[];
40
+ stale: Array<ReconcileDiskFile & {
41
+ id: number;
42
+ brain_hash: string;
43
+ }>;
44
+ orphaned: Array<{
45
+ id: number;
46
+ file_path: string;
47
+ content_hash: string | null;
48
+ }>;
49
+ renamed: Array<{
50
+ id: number;
51
+ from: string;
52
+ to: string;
53
+ }>;
54
+ /** Legacy NULL-hash rows stamped with the current file's hash this run. */
55
+ baselined: number;
56
+ /** Files seen on disk across all roots. */
57
+ scanned: number;
58
+ /** Actions actually performed (empty unless repair/removeOrphaned). */
59
+ repaired: {
60
+ relabelled: number;
61
+ reingested: number;
62
+ removed: number;
63
+ };
64
+ }
65
+ export interface ReconcileOptions {
66
+ /** Filesystem roots to walk; brain file_paths are relative to one of these. */
67
+ roots: string[];
68
+ /** Apply renames (relabel) and re-ingest missing/stale via `reingest`. */
69
+ repair?: boolean;
70
+ /** Delete orphaned rows + their clusters. Destructive — explicitly opt-in. */
71
+ removeOrphaned?: boolean;
72
+ /** Consumer-provided ingest (brain-core storeConversation wired by the consumer). */
73
+ reingest?: (file: ReconcileDiskFile) => Promise<void>;
74
+ }
75
+ export declare function reconcile(t: TenantContext, opts: ReconcileOptions): Promise<ReconcileReport>;
76
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAKjE,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC,iBAAiB,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAChF,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,QAAQ,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE;AAED,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAkBD,wBAAsB,SAAS,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqElG"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Reconcile (Spec B) — diff the brain against the filesystem and converge.
3
+ * This is the capability that catches the 42-files class: files marked
4
+ * "synced" but silently absent from the brain.
5
+ *
6
+ * REPORT-ONLY by default: a plain run returns structured findings and makes no
7
+ * destructive change. The one write a report run performs is the documented
8
+ * lazy baseline (resolved decision #2): a legacy row with NULL content_hash
9
+ * whose file exists on disk gets the CURRENT file's hash stamped and is
10
+ * classified up-to-date — trusting the present file as baseline avoids a full
11
+ * re-embed of every legacy row.
12
+ *
13
+ * Classification:
14
+ * missing — on disk, no brain row (→ repair: re-ingest via callback)
15
+ * stale — both, hashes differ (→ repair: re-ingest via callback)
16
+ * orphaned — brain row, no file on disk (→ removeOrphaned: delete cluster)
17
+ * renamed — a missing path whose hash equals an orphaned row's hash
18
+ * (→ repair: ONE file_path UPDATE — cluster, enrichment, and
19
+ * embeddings all survive; no re-ingest, no re-embed)
20
+ *
21
+ * Rename-and-edit is NOT detected (hashes differ) — it degrades to the
22
+ * missing + orphaned pair, i.e. add + remove. Documented limitation.
23
+ *
24
+ * Re-ingest is delegated to `opts.reingest` because ingestion is brain-core's
25
+ * pipeline (LLM extraction + embedding) and the dependency points the other
26
+ * way. The consumer (KBDE/KyberBot sweep, Arcana job) wires storeConversation
27
+ * into it — see the spec's consumer-integration section.
28
+ *
29
+ * Filesystem truth = the walked roots; the watched-folders manifest is never
30
+ * consulted. Dotfiles and dot-directories are skipped.
31
+ */
32
+ import { readdirSync, readFileSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { getDb } from './index.js';
35
+ import { contentHash } from './hash.js';
36
+ import { sqliteTimelineStore } from './timeline.js';
37
+ function walk(dir, relBase, acc) {
38
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
39
+ if (entry.name.startsWith('.'))
40
+ continue;
41
+ const abs = join(dir, entry.name);
42
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
43
+ if (entry.isDirectory()) {
44
+ walk(abs, rel, acc);
45
+ }
46
+ else if (entry.isFile()) {
47
+ // first root wins on cross-root relpath collision
48
+ if (!acc.has(rel)) {
49
+ acc.set(rel, { file_path: rel, absolute_path: abs, content_hash: contentHash(readFileSync(abs)) });
50
+ }
51
+ }
52
+ }
53
+ }
54
+ export async function reconcile(t, opts) {
55
+ const onDisk = new Map();
56
+ for (const root of opts.roots)
57
+ walk(root, '', onDisk);
58
+ const brainRows = await sqliteTimelineStore.listAllFilePaths(t);
59
+ const db = getDb(t, 'timeline');
60
+ const stampHash = db.prepare(`UPDATE timeline_events SET content_hash = ? WHERE id = ?`);
61
+ let baselined = 0;
62
+ const stale = [];
63
+ const orphaned = [];
64
+ const brainPaths = new Set();
65
+ for (const row of brainRows) {
66
+ brainPaths.add(row.file_path);
67
+ const disk = onDisk.get(row.file_path);
68
+ if (!disk) {
69
+ orphaned.push(row);
70
+ }
71
+ else if (row.content_hash === null) {
72
+ stampHash.run(disk.content_hash, row.id); // lazy baseline: trust the current file
73
+ baselined++;
74
+ }
75
+ else if (row.content_hash !== disk.content_hash) {
76
+ stale.push({ ...disk, id: row.id, brain_hash: row.content_hash });
77
+ }
78
+ }
79
+ let missing = [...onDisk.values()].filter((f) => !brainPaths.has(f.file_path));
80
+ // Rename detection: a missing path whose content matches an orphaned row's
81
+ // hash is the same file under a new name — relocate the LOCATOR, keep the
82
+ // cluster. (Requires the orphaned row to carry a hash; legacy NULL-hash
83
+ // orphans can't match and stay orphaned.)
84
+ const renamed = [];
85
+ const orphanByHash = new Map();
86
+ for (const o of orphaned)
87
+ if (o.content_hash)
88
+ orphanByHash.set(o.content_hash, o);
89
+ missing = missing.filter((m) => {
90
+ const o = orphanByHash.get(m.content_hash);
91
+ if (!o)
92
+ return true;
93
+ renamed.push({ id: o.id, from: o.file_path, to: m.file_path });
94
+ orphanByHash.delete(m.content_hash);
95
+ orphaned.splice(orphaned.indexOf(o), 1);
96
+ return false;
97
+ });
98
+ const repaired = { relabelled: 0, reingested: 0, removed: 0 };
99
+ if (opts.repair) {
100
+ const relabel = db.prepare(`UPDATE timeline_events SET file_path = ? WHERE id = ?`);
101
+ for (const r of renamed) {
102
+ relabel.run(r.to, r.id);
103
+ repaired.relabelled++;
104
+ }
105
+ if (opts.reingest) {
106
+ for (const f of [...missing, ...stale]) {
107
+ await opts.reingest(f);
108
+ repaired.reingested++;
109
+ }
110
+ }
111
+ }
112
+ if (opts.removeOrphaned) {
113
+ for (const o of orphaned) {
114
+ await sqliteTimelineStore.removeFileCluster(t, o.file_path, { removeEvent: true });
115
+ repaired.removed++;
116
+ }
117
+ }
118
+ return { missing, stale, orphaned, renamed, baselined, scanned: onDisk.size, repaired };
119
+ }
120
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../src/reconcile.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAOV,kBAAkB,EACnB,MAAM,6BAA6B,CAAC;AAwDrC,iEAAiE;AACjE,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAG5D;AAuGD,eAAO,MAAM,mBAAmB,EAAE,kBAiNjC,CAAC"}
1
+ {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAOV,kBAAkB,EACnB,MAAM,6BAA6B,CAAC;AA6DrC,iEAAiE;AACjE,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAG5D;AA2GD,eAAO,MAAM,mBAAmB,EAAE,kBAoRjC,CAAC"}
package/dist/timeline.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { getDb } from './index.js';
8
8
  import { sanitizeFtsQuery } from './fts-sanitizer.js';
9
+ import { lazyVectorStore } from './vec-lazy.js';
9
10
  function rowToEvent(row) {
10
11
  return {
11
12
  id: row.id,
@@ -29,6 +30,8 @@ function rowToEvent(row) {
29
30
  classification: row.classification ?? undefined,
30
31
  connection_id: row.connection_id ?? undefined,
31
32
  source_did: row.source_did ?? undefined,
33
+ file_path: row.file_path ?? undefined,
34
+ content_hash: row.content_hash ?? undefined,
32
35
  };
33
36
  }
34
37
  const initialized = new Set();
@@ -65,7 +68,9 @@ function ensureSchema(t) {
65
68
  project_id TEXT,
66
69
  classification TEXT,
67
70
  connection_id TEXT,
68
- source_did TEXT
71
+ source_did TEXT,
72
+ file_path TEXT,
73
+ content_hash TEXT
69
74
  );
70
75
 
71
76
  CREATE INDEX IF NOT EXISTS idx_timeline_timestamp ON timeline_events(timestamp);
@@ -77,6 +82,8 @@ function ensureSchema(t) {
77
82
  CREATE INDEX IF NOT EXISTS idx_timeline_project ON timeline_events(project_id);
78
83
  CREATE INDEX IF NOT EXISTS idx_timeline_classification ON timeline_events(classification);
79
84
  CREATE INDEX IF NOT EXISTS idx_timeline_connection ON timeline_events(connection_id);
85
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_timeline_file_path
86
+ ON timeline_events(file_path) WHERE file_path IS NOT NULL;
80
87
 
81
88
  CREATE VIRTUAL TABLE IF NOT EXISTS timeline_fts USING fts5(
82
89
  title, summary, entities, topics, content='',
@@ -145,13 +152,13 @@ export const sqliteTimelineStore = {
145
152
  async addEvent(t, event) {
146
153
  ensureSchema(t);
147
154
  const db = getDb(t, 'timeline');
148
- const result = db.prepare(`
149
- INSERT INTO timeline_events
150
- (type, timestamp, end_timestamp, title, summary, source_path,
151
- entities_json, topics_json,
152
- project_id, tags_json, classification, connection_id, source_did)
153
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
154
- ON CONFLICT(source_path) DO UPDATE SET
155
+ // Dual-key upsert (Spec B): file rows converge on file_path (the mutable
156
+ // locator), conversation rows on source_path — exactly as before. In the
157
+ // file branch the row ADOPTS the new ingest's source_path (the fresh
158
+ // facts/chunks cluster lives under it; exact-match cleanup removes the old
159
+ // one), while the row id and enrichment columns (priority, tier, decay,
160
+ // pin, access, last_enriched) persist by omission from the SET list.
161
+ const sharedSet = `
155
162
  type = excluded.type,
156
163
  timestamp = excluded.timestamp,
157
164
  end_timestamp = excluded.end_timestamp,
@@ -163,10 +170,60 @@ export const sqliteTimelineStore = {
163
170
  tags_json = COALESCE(excluded.tags_json, timeline_events.tags_json),
164
171
  classification = COALESCE(excluded.classification, timeline_events.classification),
165
172
  connection_id = COALESCE(excluded.connection_id, timeline_events.connection_id),
166
- source_did = COALESCE(excluded.source_did, timeline_events.source_did)
167
- `).run(event.type, event.timestamp, event.end_timestamp ?? null, event.title, event.summary ?? '', event.source_path, JSON.stringify(event.entities), JSON.stringify(event.topics), event.project_id ?? null, event.tags ? JSON.stringify(event.tags) : null, event.classification ?? null, event.connection_id ?? null, event.source_did ?? null);
173
+ source_did = COALESCE(excluded.source_did, timeline_events.source_did)`;
174
+ const conflictClause = event.file_path
175
+ ? `ON CONFLICT(file_path) WHERE file_path IS NOT NULL DO UPDATE SET
176
+ source_path = excluded.source_path,
177
+ content_hash = excluded.content_hash,${sharedSet}`
178
+ : `ON CONFLICT(source_path) DO UPDATE SET
179
+ file_path = COALESCE(excluded.file_path, timeline_events.file_path),
180
+ content_hash = COALESCE(excluded.content_hash, timeline_events.content_hash),${sharedSet}`;
181
+ const result = db.prepare(`
182
+ INSERT INTO timeline_events
183
+ (type, timestamp, end_timestamp, title, summary, source_path,
184
+ entities_json, topics_json,
185
+ project_id, tags_json, classification, connection_id, source_did,
186
+ file_path, content_hash)
187
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
188
+ ${conflictClause}
189
+ `).run(event.type, event.timestamp, event.end_timestamp ?? null, event.title, event.summary ?? '', event.source_path, JSON.stringify(event.entities), JSON.stringify(event.topics), event.project_id ?? null, event.tags ? JSON.stringify(event.tags) : null, event.classification ?? null, event.connection_id ?? null, event.source_did ?? null, event.file_path ?? null, event.content_hash ?? null);
168
190
  return result.lastInsertRowid;
169
191
  },
192
+ async removeFileCluster(t, filePath, opts = {}) {
193
+ ensureSchema(t);
194
+ const db = getDb(t, 'timeline');
195
+ // EXACT match only — the substring approach this replaces cross-deleted
196
+ // notes.md when re-ingesting meeting-notes.md (baseline bug #14).
197
+ const row = db
198
+ .prepare(`SELECT id, source_path FROM timeline_events WHERE file_path = ?`)
199
+ .get(filePath);
200
+ if (!row)
201
+ return { removed: false, facts: 0, chunks: 0 };
202
+ // source_path = channel://<channel>/<conversationId> — the cluster key.
203
+ const conversationId = row.source_path.split('/').pop() ?? '';
204
+ const facts = conversationId
205
+ ? db.prepare(`DELETE FROM facts WHERE source_conversation_id = ?`).run(conversationId).changes
206
+ : 0;
207
+ const chunks = conversationId
208
+ ? await lazyVectorStore.deleteByOriginPrefix(t, `${conversationId}_seg_`)
209
+ : 0;
210
+ if (opts.removeEvent) {
211
+ db.prepare(`DELETE FROM timeline_events WHERE id = ?`).run(row.id);
212
+ }
213
+ return { removed: true, facts, chunks };
214
+ },
215
+ async listAllFilePaths(t, opts = {}) {
216
+ ensureSchema(t);
217
+ const db = getDb(t, 'timeline');
218
+ return db
219
+ .prepare(`
220
+ SELECT id, file_path, content_hash FROM timeline_events
221
+ WHERE type = 'file' AND file_path IS NOT NULL AND id > ?
222
+ ORDER BY id ASC
223
+ LIMIT ?
224
+ `)
225
+ .all(opts.afterId ?? 0, opts.limit ?? -1);
226
+ },
170
227
  async removeEventByPath(t, sourcePath) {
171
228
  ensureSchema(t);
172
229
  const db = getDb(t, 'timeline');