memkin 0.4.0 → 0.4.1

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 (129) hide show
  1. package/README.en.md +108 -836
  2. package/README.md +108 -836
  3. package/README.zh-CN.md +80 -44
  4. package/dist/cli-stores.d.ts +12 -0
  5. package/dist/cli-stores.d.ts.map +1 -1
  6. package/dist/cli-stores.js +15 -1
  7. package/dist/cli-stores.js.map +1 -1
  8. package/dist/cli.js +94 -6
  9. package/dist/cli.js.map +1 -1
  10. package/dist/collectors/agent/claude-code.d.ts.map +1 -1
  11. package/dist/collectors/agent/claude-code.js +10 -29
  12. package/dist/collectors/agent/claude-code.js.map +1 -1
  13. package/dist/collectors/agent/codex.d.ts.map +1 -1
  14. package/dist/collectors/agent/codex.js +24 -10
  15. package/dist/collectors/agent/codex.js.map +1 -1
  16. package/dist/collectors/agent/collector.d.ts +38 -0
  17. package/dist/collectors/agent/collector.d.ts.map +1 -1
  18. package/dist/collectors/agent/collector.js +91 -20
  19. package/dist/collectors/agent/collector.js.map +1 -1
  20. package/dist/collectors/agent/hermes.d.ts.map +1 -1
  21. package/dist/collectors/agent/hermes.js +5 -1
  22. package/dist/collectors/agent/hermes.js.map +1 -1
  23. package/dist/collectors/feishu/sources/calendar.d.ts.map +1 -1
  24. package/dist/collectors/feishu/sources/calendar.js +18 -7
  25. package/dist/collectors/feishu/sources/calendar.js.map +1 -1
  26. package/dist/consolidator/consolidator.d.ts +9 -1
  27. package/dist/consolidator/consolidator.d.ts.map +1 -1
  28. package/dist/consolidator/consolidator.js +7 -0
  29. package/dist/consolidator/consolidator.js.map +1 -1
  30. package/dist/consolidator/entity-merge.d.ts +33 -0
  31. package/dist/consolidator/entity-merge.d.ts.map +1 -0
  32. package/dist/consolidator/entity-merge.js +148 -0
  33. package/dist/consolidator/entity-merge.js.map +1 -0
  34. package/dist/core/agent-session-scanner.d.ts +23 -0
  35. package/dist/core/agent-session-scanner.d.ts.map +1 -0
  36. package/dist/core/agent-session-scanner.js +69 -0
  37. package/dist/core/agent-session-scanner.js.map +1 -0
  38. package/dist/core/canonicalize.d.ts +8 -0
  39. package/dist/core/canonicalize.d.ts.map +1 -1
  40. package/dist/core/canonicalize.js +12 -2
  41. package/dist/core/canonicalize.js.map +1 -1
  42. package/dist/core/config.d.ts +6 -0
  43. package/dist/core/config.d.ts.map +1 -1
  44. package/dist/core/config.js.map +1 -1
  45. package/dist/core/identity-resolver.d.ts +53 -3
  46. package/dist/core/identity-resolver.d.ts.map +1 -1
  47. package/dist/core/identity-resolver.js +112 -17
  48. package/dist/core/identity-resolver.js.map +1 -1
  49. package/dist/core/person-identity.d.ts +38 -3
  50. package/dist/core/person-identity.d.ts.map +1 -1
  51. package/dist/core/person-identity.js +58 -29
  52. package/dist/core/person-identity.js.map +1 -1
  53. package/dist/core/pipeline-factory.d.ts.map +1 -1
  54. package/dist/core/pipeline-factory.js +2 -1
  55. package/dist/core/pipeline-factory.js.map +1 -1
  56. package/dist/core/pipeline.js +3 -3
  57. package/dist/core/pipeline.js.map +1 -1
  58. package/dist/daemon/autostart/daemon-state.d.ts +32 -0
  59. package/dist/daemon/autostart/daemon-state.d.ts.map +1 -1
  60. package/dist/daemon/autostart/daemon-state.js +27 -1
  61. package/dist/daemon/autostart/daemon-state.js.map +1 -1
  62. package/dist/daemon/scheduler.d.ts +10 -0
  63. package/dist/daemon/scheduler.d.ts.map +1 -1
  64. package/dist/daemon/scheduler.js +20 -1
  65. package/dist/daemon/scheduler.js.map +1 -1
  66. package/dist/demo/seed.d.ts +32 -0
  67. package/dist/demo/seed.d.ts.map +1 -0
  68. package/dist/demo/seed.js +185 -0
  69. package/dist/demo/seed.js.map +1 -0
  70. package/dist/embedded-assets.generated.d.ts.map +1 -1
  71. package/dist/embedded-assets.generated.js +2 -2
  72. package/dist/embedded-assets.generated.js.map +1 -1
  73. package/dist/eval/golden.d.ts +107 -0
  74. package/dist/eval/golden.d.ts.map +1 -0
  75. package/dist/eval/golden.js +74 -0
  76. package/dist/eval/golden.js.map +1 -0
  77. package/dist/eval/judge.d.ts +82 -0
  78. package/dist/eval/judge.d.ts.map +1 -0
  79. package/dist/eval/judge.js +130 -0
  80. package/dist/eval/judge.js.map +1 -0
  81. package/dist/eval/manifest.d.ts +107 -0
  82. package/dist/eval/manifest.d.ts.map +1 -0
  83. package/dist/eval/manifest.js +84 -0
  84. package/dist/eval/manifest.js.map +1 -0
  85. package/dist/eval/metrics.d.ts +87 -0
  86. package/dist/eval/metrics.d.ts.map +1 -0
  87. package/dist/eval/metrics.js +97 -0
  88. package/dist/eval/metrics.js.map +1 -0
  89. package/dist/extractors/noise-filter.d.ts +7 -3
  90. package/dist/extractors/noise-filter.d.ts.map +1 -1
  91. package/dist/extractors/noise-filter.js +16 -3
  92. package/dist/extractors/noise-filter.js.map +1 -1
  93. package/dist/extractors/signal-extractor.d.ts.map +1 -1
  94. package/dist/extractors/signal-extractor.js +13 -2
  95. package/dist/extractors/signal-extractor.js.map +1 -1
  96. package/dist/lifecycle/legacy-migration.d.ts +6 -2
  97. package/dist/lifecycle/legacy-migration.d.ts.map +1 -1
  98. package/dist/lifecycle/legacy-migration.js +69 -4
  99. package/dist/lifecycle/legacy-migration.js.map +1 -1
  100. package/dist/server/api.d.ts.map +1 -1
  101. package/dist/server/api.js +18 -7
  102. package/dist/server/api.js.map +1 -1
  103. package/dist/server/backfill-routes.d.ts.map +1 -1
  104. package/dist/server/backfill-routes.js +2 -1
  105. package/dist/server/backfill-routes.js.map +1 -1
  106. package/dist/server/mcp.d.ts +4 -4
  107. package/dist/store/agent-sessions.d.ts +59 -0
  108. package/dist/store/agent-sessions.d.ts.map +1 -0
  109. package/dist/store/agent-sessions.js +119 -0
  110. package/dist/store/agent-sessions.js.map +1 -0
  111. package/dist/store/database.d.ts +20 -1
  112. package/dist/store/database.d.ts.map +1 -1
  113. package/dist/store/database.js +44 -6
  114. package/dist/store/database.js.map +1 -1
  115. package/dist/store/embedding.d.ts +20 -2
  116. package/dist/store/embedding.d.ts.map +1 -1
  117. package/dist/store/embedding.js +20 -5
  118. package/dist/store/embedding.js.map +1 -1
  119. package/dist/store/entity-suggestions.d.ts +62 -0
  120. package/dist/store/entity-suggestions.d.ts.map +1 -0
  121. package/dist/store/entity-suggestions.js +80 -0
  122. package/dist/store/entity-suggestions.js.map +1 -0
  123. package/dist/store/migrations/index.d.ts.map +1 -1
  124. package/dist/store/migrations/index.js +91 -0
  125. package/dist/store/migrations/index.js.map +1 -1
  126. package/dist/store/pages.d.ts.map +1 -1
  127. package/dist/store/pages.js +12 -3
  128. package/dist/store/pages.js.map +1 -1
  129. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ // src/store/agent-sessions.ts
2
+ //
3
+ // AgentSessionStore — the processing ledger for agent transcript sources
4
+ // (claude-code / codex / hermes). One row per session REVISION, keyed by
5
+ // (source_instance, session_id, content_hash). See migration M007.
6
+ //
7
+ // This ledger replaces the lossy per-agent cursor watermark: revisions are
8
+ // detected by content_hash change (not lexicographic sessionId comparison),
9
+ // and each session's processing state is tracked independently of the scan
10
+ // watermark so a poison session never blocks progress.
11
+ // Allowed state transitions. Enforced by markState; an unlisted edge throws.
12
+ const ALLOWED_TRANSITIONS = {
13
+ discovered: ["distilled"],
14
+ distilled: ["applying", "retrying"],
15
+ applying: ["done", "retrying"],
16
+ retrying: ["distilled", "dead_letter"],
17
+ done: [],
18
+ dead_letter: [],
19
+ };
20
+ function rowToRevision(row) {
21
+ return {
22
+ id: row.id,
23
+ sourceInstance: row.source_instance,
24
+ sessionId: row.session_id,
25
+ contentHash: row.content_hash,
26
+ byteSize: typeof row.byte_size === "string" ? Number(row.byte_size) : row.byte_size,
27
+ lineCount: row.line_count,
28
+ state: row.state,
29
+ retryCount: row.retry_count,
30
+ payloadId: row.payload_id,
31
+ stagingAppliedAt: row.staging_applied_at,
32
+ prodAppliedAt: row.prod_applied_at,
33
+ discoveredAt: row.discovered_at,
34
+ updatedAt: row.updated_at,
35
+ };
36
+ }
37
+ export class AgentSessionStore {
38
+ pg;
39
+ constructor(pg) {
40
+ this.pg = pg;
41
+ }
42
+ /**
43
+ * Idempotently record a session revision.
44
+ * - No row for (source, session, hash) yet, and no prior hash for this session → "new"
45
+ * - No row for this exact hash but the session already has other revisions → "revised"
46
+ * - Exact (source, session, hash) already present → "unchanged"
47
+ */
48
+ async recordRevision(input) {
49
+ const inserted = await this.pg.query(`INSERT INTO agent_sessions
50
+ (source_instance, session_id, content_hash, byte_size, line_count)
51
+ VALUES ($1, $2, $3, $4, $5)
52
+ ON CONFLICT (source_instance, session_id, content_hash) DO NOTHING
53
+ RETURNING *`, [input.sourceInstance, input.sessionId, input.contentHash, input.byteSize, input.lineCount]);
54
+ if (inserted.rows.length === 0) {
55
+ // Exact revision already exists.
56
+ const existing = await this.pg.query(`SELECT * FROM agent_sessions
57
+ WHERE source_instance = $1 AND session_id = $2 AND content_hash = $3`, [input.sourceInstance, input.sessionId, input.contentHash]);
58
+ return { status: "unchanged", revision: rowToRevision(existing.rows[0]) };
59
+ }
60
+ const revision = rowToRevision(inserted.rows[0]);
61
+ // Did this session have prior revisions (different hashes)?
62
+ const priorCount = await this.pg.query(`SELECT COUNT(*)::int AS n FROM agent_sessions
63
+ WHERE source_instance = $1 AND session_id = $2 AND content_hash <> $3`, [input.sourceInstance, input.sessionId, input.contentHash]);
64
+ const status = priorCount.rows[0].n > 0 ? "revised" : "new";
65
+ return { status, revision };
66
+ }
67
+ async getLatestRevision(sourceInstance, sessionId) {
68
+ const r = await this.pg.query(`SELECT * FROM agent_sessions
69
+ WHERE source_instance = $1 AND session_id = $2
70
+ ORDER BY discovered_at DESC, id DESC
71
+ LIMIT 1`, [sourceInstance, sessionId]);
72
+ return r.rows[0] ? rowToRevision(r.rows[0]) : null;
73
+ }
74
+ async getRevision(id) {
75
+ const r = await this.pg.query("SELECT * FROM agent_sessions WHERE id = $1", [
76
+ id,
77
+ ]);
78
+ return r.rows[0] ? rowToRevision(r.rows[0]) : null;
79
+ }
80
+ async listSessions(opts) {
81
+ const clauses = [];
82
+ const params = [];
83
+ if (opts?.sourceInstance) {
84
+ params.push(opts.sourceInstance);
85
+ clauses.push(`source_instance = $${params.length}`);
86
+ }
87
+ if (opts?.state) {
88
+ params.push(opts.state);
89
+ clauses.push(`state = $${params.length}`);
90
+ }
91
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
92
+ const r = await this.pg.query(`SELECT * FROM agent_sessions ${where} ORDER BY discovered_at DESC, id DESC`, params);
93
+ return r.rows.map(rowToRevision);
94
+ }
95
+ /**
96
+ * Transition a revision to a new state, enforcing the state machine.
97
+ * Throws on an illegal transition. Refreshes updated_at.
98
+ */
99
+ async markState(id, nextState) {
100
+ const current = await this.getRevision(id);
101
+ if (!current)
102
+ throw new Error(`agent_sessions row ${id} not found`);
103
+ const allowed = ALLOWED_TRANSITIONS[current.state];
104
+ if (!allowed.includes(nextState)) {
105
+ throw new Error(`illegal state transition: ${current.state} → ${nextState} (row ${id})`);
106
+ }
107
+ const r = await this.pg.query(`UPDATE agent_sessions SET state = $2, updated_at = NOW() WHERE id = $1 RETURNING *`, [id, nextState]);
108
+ return rowToRevision(r.rows[0]);
109
+ }
110
+ /** Increment retry_count (and refresh updated_at). Threshold policy lives in the caller. */
111
+ async incrementRetry(id) {
112
+ const r = await this.pg.query(`UPDATE agent_sessions SET retry_count = retry_count + 1, updated_at = NOW()
113
+ WHERE id = $1 RETURNING retry_count`, [id]);
114
+ if (r.rows.length === 0)
115
+ throw new Error(`agent_sessions row ${id} not found`);
116
+ return r.rows[0].retry_count;
117
+ }
118
+ }
119
+ //# sourceMappingURL=agent-sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-sessions.js","sourceRoot":"","sources":["../../src/store/agent-sessions.ts"],"names":[],"mappings":"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"}
@@ -14,10 +14,29 @@ export interface DatabaseOptions {
14
14
  embeddingDimensions?: number;
15
15
  lockLabel?: string;
16
16
  }
17
+ /** Recorded when the database was embedded with a different provider/model/dims than the config asks for. */
18
+ export interface EmbeddingMismatch {
19
+ /** Fingerprint stored in the database (provider:model:dimensions). */
20
+ have: string;
21
+ /** Fingerprint the current config would produce. */
22
+ want: string;
23
+ }
24
+ /**
25
+ * Build the actionable error thrown when an embedding operation runs against
26
+ * a database whose stored fingerprint differs from the current config.
27
+ */
28
+ export declare function embeddingMismatchError(mismatch: EmbeddingMismatch): Error;
17
29
  export declare class Database {
18
30
  readonly executor: SqlExecutor;
19
31
  readonly embeddingDimensions: number;
32
+ private readonly embeddingMismatch;
20
33
  private constructor();
34
+ /**
35
+ * Throw if the database's embedding fingerprint diverges from the config
36
+ * this Database was opened with. Called lazily — from EmbeddingService's
37
+ * first-use guard — so read-only commands are never blocked by a mismatch.
38
+ */
39
+ assertEmbeddingConsistent(): void;
21
40
  static create(dataDirOrConfig?: string | Config, opts?: DatabaseOptions): Promise<Database>;
22
41
  close(): Promise<void>;
23
42
  }
@@ -27,5 +46,5 @@ export declare class Database {
27
46
  *
28
47
  * @internal — test use only.
29
48
  */
30
- export declare function ensureEmbeddingConsistencyForTest(conn: SqlConn, targetDims: number, config: Config): Promise<void>;
49
+ export declare function ensureEmbeddingConsistencyForTest(conn: SqlConn, targetDims: number, config: Config): Promise<EmbeddingMismatch | null>;
31
50
  //# sourceMappingURL=database.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAKhD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG9D,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE,MAAqC,GAAG,MAAM,CAEjF;AAED,MAAM,WAAW,eAAe;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,QAAQ;IAEjB,QAAQ,CAAC,QAAQ,EAAE,WAAW;IAC9B,QAAQ,CAAC,mBAAmB,EAAE,MAAM;IAFtC,OAAO;WAKM,MAAM,CACjB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EACjC,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,QAAQ,CAAC;IAkCd,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAgED;;;;;GAKG;AACH,wBAAsB,iCAAiC,CACrD,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAEf"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAKhD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG9D,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE,MAAqC,GAAG,MAAM,CAEjF;AAED,MAAM,WAAW,eAAe;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,6GAA6G;AAC7G,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,KAAK,CAWzE;AAED,qBAAa,QAAQ;IAEjB,QAAQ,CAAC,QAAQ,EAAE,WAAW;IAC9B,QAAQ,CAAC,mBAAmB,EAAE,MAAM;IACpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAHpC,OAAO;IAMP;;;;OAIG;IACH,yBAAyB,IAAI,IAAI;WAIpB,MAAM,CACjB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EACjC,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,QAAQ,CAAC;IAmCd,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AA0ED;;;;;GAKG;AACH,wBAAsB,iCAAiC,CACrD,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAEnC"}
@@ -15,12 +15,37 @@ export const DEFAULT_EMBEDDING_DIMENSIONS = 1536;
15
15
  export function loadSchemaSql(dims = DEFAULT_EMBEDDING_DIMENSIONS) {
16
16
  return SCHEMA_SQL.replace("__EMBEDDING_DIM__", String(dims));
17
17
  }
18
+ /**
19
+ * Build the actionable error thrown when an embedding operation runs against
20
+ * a database whose stored fingerprint differs from the current config.
21
+ */
22
+ export function embeddingMismatchError(mismatch) {
23
+ return new Error(`Embedding fingerprint mismatch: database has "${mismatch.have}" but config wants "${mismatch.want}". ` +
24
+ "Refusing to run embedding operations against a mismatched index (mixing models silently corrupts search results).\n" +
25
+ "Fix one of two ways:\n" +
26
+ ` 1. Revert the embedding settings in your config to match the database: provider:model:dimensions = "${mismatch.have}".\n` +
27
+ " 2. Fully re-embed with the new model: clear the stored vectors and fingerprint —\n" +
28
+ " UPDATE content_chunks SET embedding = NULL, embedded_at = NULL;\n" +
29
+ " DELETE FROM memkin_meta WHERE key = 'embedding_fingerprint';\n" +
30
+ " then reopen and run `memkin embed` to rebuild all embeddings.");
31
+ }
18
32
  export class Database {
19
33
  executor;
20
34
  embeddingDimensions;
21
- constructor(executor, embeddingDimensions) {
35
+ embeddingMismatch;
36
+ constructor(executor, embeddingDimensions, embeddingMismatch = null) {
22
37
  this.executor = executor;
23
38
  this.embeddingDimensions = embeddingDimensions;
39
+ this.embeddingMismatch = embeddingMismatch;
40
+ }
41
+ /**
42
+ * Throw if the database's embedding fingerprint diverges from the config
43
+ * this Database was opened with. Called lazily — from EmbeddingService's
44
+ * first-use guard — so read-only commands are never blocked by a mismatch.
45
+ */
46
+ assertEmbeddingConsistent() {
47
+ if (this.embeddingMismatch)
48
+ throw embeddingMismatchError(this.embeddingMismatch);
24
49
  }
25
50
  static async create(dataDirOrConfig, opts) {
26
51
  const config = typeof dataDirOrConfig === "string" || dataDirOrConfig === undefined
@@ -28,6 +53,7 @@ export class Database {
28
53
  : dataDirOrConfig;
29
54
  const dims = opts?.embeddingDimensions ?? DEFAULT_EMBEDDING_DIMENSIONS;
30
55
  let executor;
56
+ let mismatch = null;
31
57
  try {
32
58
  executor = await createEngine(config);
33
59
  const e = executor;
@@ -42,9 +68,9 @@ export class Database {
42
68
  console.warn(`[memoark] Repaired id sequence desync (likely an id-preserving import/restore): ` +
43
69
  repaired.map((r) => `${r.table} → next id ${r.maxId + 1}`).join(", "));
44
70
  }
45
- await ensureEmbeddingConsistency(conn, dims, config);
71
+ mismatch = await ensureEmbeddingConsistency(conn, dims, config);
46
72
  });
47
- return new Database(e, dims);
73
+ return new Database(e, dims, mismatch);
48
74
  }
49
75
  catch (err) {
50
76
  await executor?.close().catch(() => { });
@@ -80,6 +106,14 @@ async function migrateEmbeddingDimensions(conn, targetDims) {
80
106
  USING hnsw (embedding vector_cosine_ops);
81
107
  `);
82
108
  }
109
+ /**
110
+ * Reconcile the database's stored embedding fingerprint with the current config.
111
+ *
112
+ * Returns the mismatch (never throws, never rewrites the stored fingerprint)
113
+ * so read-only commands — `search --mode fts`, `export` — can still open the
114
+ * database. Embedding paths surface the mismatch lazily on first use via
115
+ * Database.assertEmbeddingConsistent().
116
+ */
83
117
  async function ensureEmbeddingConsistency(conn, targetDims, config) {
84
118
  const fp = {
85
119
  provider: config.embedding?.provider ?? "openai",
@@ -92,14 +126,18 @@ async function ensureEmbeddingConsistency(conn, targetDims, config) {
92
126
  // Fresh database — migrate dimensions if needed, then record fingerprint.
93
127
  await migrateEmbeddingDimensions(conn, targetDims);
94
128
  await writeMeta(conn, "embedding_fingerprint", want);
95
- return;
129
+ return null;
96
130
  }
97
131
  if (have !== want) {
98
- throw new Error(`Embedding fingerprint mismatch: db="${have}" config="${want}". ` +
99
- "不静默改写共享库。改回原 embedding 配置或跑显式 reindex。");
132
+ // Do NOT silently rewrite the shared library's fingerprint, and do NOT
133
+ // run the dimension migration (it would wipe embeddings that are still
134
+ // valid for the recorded model). Record the divergence; embedding
135
+ // operations will refuse to run until the user resolves it.
136
+ return { have, want };
100
137
  }
101
138
  // Fingerprint matches — still run dimension migration guard (idempotent).
102
139
  await migrateEmbeddingDimensions(conn, targetDims);
140
+ return null;
103
141
  }
104
142
  /**
105
143
  * Exported for unit-testing the fingerprint logic without requiring a
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"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"}
@@ -6,12 +6,30 @@ export interface EmbeddingConfig {
6
6
  apiKey?: string;
7
7
  baseUrl?: string;
8
8
  }
9
+ export interface EmbeddingServiceDeps {
10
+ /**
11
+ * Guard invoked before every real embedding operation, ahead of any client
12
+ * construction or database access. Used to defer embedding-fingerprint
13
+ * validation (Database.assertEmbeddingConsistent) to first actual use, so
14
+ * read-only commands (search FTS, export) can open a mismatched database
15
+ * without being blocked and without embedding credentials.
16
+ */
17
+ beforeFirstUse?: () => void;
18
+ }
9
19
  export declare class EmbeddingService {
10
20
  private pg;
11
- private client;
21
+ private deps;
22
+ /**
23
+ * Lazily constructed on first use — building the OpenAI client eagerly
24
+ * would demand credentials even on code paths that never embed anything.
25
+ */
26
+ private client?;
27
+ private clientOptions;
12
28
  private model;
13
29
  private dimensions;
14
- constructor(pg: SqlConn, config: EmbeddingConfig);
30
+ constructor(pg: SqlConn, config: EmbeddingConfig, deps?: EmbeddingServiceDeps);
31
+ /** Run the consistency guard, then build (or reuse) the OpenAI client. */
32
+ private ensureClient;
15
33
  embedStale(opts?: {
16
34
  limit?: number;
17
35
  }): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"embedding.d.ts","sourceRoot":"","sources":["../../src/store/embedding.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,qBAAa,gBAAgB;IAMzB,OAAO,CAAC,EAAE;IALZ,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,UAAU,CAAS;gBAGjB,EAAE,EAAE,OAAO,EACnB,MAAM,EAAE,eAAe;IAWnB,UAAU,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA0CpF,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAQjD"}
1
+ {"version":3,"file":"embedding.d.ts","sourceRoot":"","sources":["../../src/store/embedding.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAID,qBAAa,gBAAgB;IAWzB,OAAO,CAAC,EAAE;IAEV,OAAO,CAAC,IAAI;IAZd;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,UAAU,CAAS;gBAGjB,EAAE,EAAE,OAAO,EACnB,MAAM,EAAE,eAAe,EACf,IAAI,GAAE,oBAAyB;IAWzC,0EAA0E;IAC1E,OAAO,CAAC,YAAY;IAMd,UAAU,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA2CpF,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CASjD"}
@@ -2,20 +2,34 @@ import OpenAI from "openai";
2
2
  const BATCH_SIZE = 100;
3
3
  export class EmbeddingService {
4
4
  pg;
5
+ deps;
6
+ /**
7
+ * Lazily constructed on first use — building the OpenAI client eagerly
8
+ * would demand credentials even on code paths that never embed anything.
9
+ */
5
10
  client;
11
+ clientOptions;
6
12
  model;
7
13
  dimensions;
8
- constructor(pg, config) {
14
+ constructor(pg, config, deps = {}) {
9
15
  this.pg = pg;
16
+ this.deps = deps;
10
17
  this.model = config.model ?? "text-embedding-3-large";
11
18
  this.dimensions = config.dimensions ?? 1536;
12
19
  const isOllama = config.provider === "ollama";
13
- this.client = new OpenAI({
20
+ this.clientOptions = {
14
21
  apiKey: isOllama ? (config.apiKey ?? "ollama") : (config.apiKey ?? ""),
15
22
  baseURL: isOllama ? (config.baseUrl ?? "http://localhost:11434/v1") : config.baseUrl,
16
- });
23
+ };
24
+ }
25
+ /** Run the consistency guard, then build (or reuse) the OpenAI client. */
26
+ ensureClient() {
27
+ this.deps.beforeFirstUse?.();
28
+ this.client ??= new OpenAI(this.clientOptions);
29
+ return this.client;
17
30
  }
18
31
  async embedStale(opts) {
32
+ const client = this.ensureClient();
19
33
  const limit = opts?.limit;
20
34
  let sql = "SELECT id, chunk_text FROM content_chunks WHERE embedded_at IS NULL ORDER BY id";
21
35
  const params = [];
@@ -32,7 +46,7 @@ export class EmbeddingService {
32
46
  const batch = stale.rows.slice(i, i + BATCH_SIZE);
33
47
  const texts = batch.map((r) => r.chunk_text);
34
48
  try {
35
- const response = await this.client.embeddings.create({
49
+ const response = await client.embeddings.create({
36
50
  model: this.model,
37
51
  input: texts,
38
52
  dimensions: this.dimensions,
@@ -51,7 +65,8 @@ export class EmbeddingService {
51
65
  return { embedded, errors };
52
66
  }
53
67
  async embedText(text) {
54
- const response = await this.client.embeddings.create({
68
+ const client = this.ensureClient();
69
+ const response = await client.embeddings.create({
55
70
  model: this.model,
56
71
  input: text,
57
72
  dimensions: this.dimensions,
@@ -1 +1 @@
1
- {"version":3,"file":"embedding.js","sourceRoot":"","sources":["../../src/store/embedding.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"embedding.js","sourceRoot":"","sources":["../../src/store/embedding.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAsB5B,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB,MAAM,OAAO,gBAAgB;IAWjB;IAEA;IAZV;;;OAGG;IACK,MAAM,CAAU;IAChB,aAAa,CAAuC;IACpD,KAAK,CAAS;IACd,UAAU,CAAS;IAE3B,YACU,EAAW,EACnB,MAAuB,EACf,OAA6B,EAAE;QAF/B,OAAE,GAAF,EAAE,CAAS;QAEX,SAAI,GAAJ,IAAI,CAA2B;QAEvC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,wBAAwB,CAAC;QACtD,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC;QAC5C,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC;QAC9C,IAAI,CAAC,aAAa,GAAG;YACnB,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;YACtE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,2BAA2B,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO;SACrF,CAAC;IACJ,CAAC;IAED,0EAA0E;IAClE,YAAY;QAClB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAAyB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC;QAC1B,IAAI,GAAG,GAAG,iFAAiF,CAAC;QAC5F,MAAM,MAAM,GAAc,EAAE,CAAC;QAC7B,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,IAAI,WAAW,CAAC;YACnB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QAE/D,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC;YACvD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,UAAU,CAG9C,CAAC;YACH,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;YAC7C,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;oBAC9C,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,KAAK,EAAE,KAAK;oBACZ,UAAU,EAAE,IAAI,CAAC,UAAU;iBAC5B,CAAC,CAAC;gBACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACtC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;oBACvC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;oBACpC,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CACjB,iGAAiG,EACjG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAClC,CAAC;oBACF,QAAQ,EAAE,CAAC;gBACb,CAAC;YACH,CAAC;YAAC,OAAO,IAAI,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;YACzB,CAAC;QACH,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAY;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;YAC9C,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpC,CAAC;CACF"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * EntityMergeSuggestionStore — persistence for entity merge suggestions
3
+ * (extraction-quality-redesign PR-3, spec §9).
4
+ *
5
+ * Near-duplicate detection (exact same name, Levenshtein-close titles,
6
+ * pinyin-equivalent person names, cross-type name clashes) NEVER merges
7
+ * automatically. It records suggestion rows here; the consolidator sweep
8
+ * aggregates them; the user reviews and either dismisses a suggestion or
9
+ * accepts it and runs the explicit merge machinery
10
+ * (PersonIdentityStore.merge). Accepting a suggestion does not itself execute
11
+ * the merge — merges stay explicit per the identity contract.
12
+ */
13
+ import type { EntityHandleType } from "../core/person-identity.js";
14
+ import type { SqlExecutor } from "./sql-executor.js";
15
+ export type MergeSuggestionReason = "same_name" | "cross_type_name" | "levenshtein" | "pinyin";
16
+ export type MergeSuggestionStatus = "pending" | "accepted" | "dismissed";
17
+ /** Snapshot row of an entity page, used by the consolidator merge sweep. */
18
+ export interface EntityPageRef {
19
+ slug: string;
20
+ type: string;
21
+ title: string;
22
+ }
23
+ export interface MergeSuggestionCandidate {
24
+ entity_type: EntityHandleType;
25
+ from_slug: string;
26
+ into_slug: string;
27
+ reason: MergeSuggestionReason;
28
+ detail?: Record<string, unknown>;
29
+ }
30
+ export interface MergeSuggestionRow extends Omit<MergeSuggestionCandidate, "detail"> {
31
+ id: number;
32
+ detail: Record<string, unknown> | null;
33
+ status: MergeSuggestionStatus;
34
+ created_at: string;
35
+ resolved_at: string | null;
36
+ }
37
+ export declare class EntityMergeSuggestionStore {
38
+ private db;
39
+ constructor(db: SqlExecutor);
40
+ /** Snapshot of all entity pages (slug/type/title) for the merge sweep. */
41
+ listEntityPages(): Promise<EntityPageRef[]>;
42
+ /**
43
+ * Record a suggestion candidate. Idempotent on
44
+ * (entity_type, from_slug, into_slug, reason): re-recording a pending row
45
+ * refreshes its detail; a resolved (accepted/dismissed) row is NEVER
46
+ * resurrected — the sweep keeps re-detecting the same pairs and a user
47
+ * dismissal must stick.
48
+ */
49
+ record(candidate: MergeSuggestionCandidate): Promise<void>;
50
+ /** List pending suggestions, optionally filtered by entity type. */
51
+ listPending(filter?: {
52
+ entityType?: EntityHandleType;
53
+ }): Promise<MergeSuggestionRow[]>;
54
+ /** Fetch a single suggestion by id. */
55
+ get(id: number): Promise<MergeSuggestionRow | null>;
56
+ /**
57
+ * Resolve a pending suggestion. `accepted` records the user's confirmation —
58
+ * the actual merge is executed separately via the explicit merge machinery.
59
+ */
60
+ resolve(id: number, status: "accepted" | "dismissed"): Promise<void>;
61
+ }
62
+ //# sourceMappingURL=entity-suggestions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-suggestions.d.ts","sourceRoot":"","sources":["../../src/store/entity-suggestions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,iBAAiB,GAAG,aAAa,GAAG,QAAQ,CAAC;AAC/F,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;AAEzE,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAID,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,wBAAwB,EAAE,QAAQ,CAAC;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAcD,qBAAa,0BAA0B;IACzB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,WAAW;IAEnC,0EAA0E;IACpE,eAAe,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAQjD;;;;;;OAMG;IACG,MAAM,CAAC,SAAS,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBhE,oEAAoE;IAC9D,WAAW,CAAC,MAAM,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,gBAAgB,CAAA;KAAE,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAiB5F,uCAAuC;IACjC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IASzD;;;OAGG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ3E"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * EntityMergeSuggestionStore — persistence for entity merge suggestions
3
+ * (extraction-quality-redesign PR-3, spec §9).
4
+ *
5
+ * Near-duplicate detection (exact same name, Levenshtein-close titles,
6
+ * pinyin-equivalent person names, cross-type name clashes) NEVER merges
7
+ * automatically. It records suggestion rows here; the consolidator sweep
8
+ * aggregates them; the user reviews and either dismisses a suggestion or
9
+ * accepts it and runs the explicit merge machinery
10
+ * (PersonIdentityStore.merge). Accepting a suggestion does not itself execute
11
+ * the merge — merges stay explicit per the identity contract.
12
+ */
13
+ const ENTITY_PAGE_TYPES = ["person", "project", "organization", "tool", "concept"];
14
+ function toRow(r) {
15
+ return {
16
+ ...r,
17
+ detail: typeof r.detail === "string" ? JSON.parse(r.detail) : r.detail,
18
+ };
19
+ }
20
+ export class EntityMergeSuggestionStore {
21
+ db;
22
+ constructor(db) {
23
+ this.db = db;
24
+ }
25
+ /** Snapshot of all entity pages (slug/type/title) for the merge sweep. */
26
+ async listEntityPages() {
27
+ const r = await this.db.query("SELECT slug, type, title FROM pages WHERE type = ANY($1)", [[...ENTITY_PAGE_TYPES]]);
28
+ return r.rows;
29
+ }
30
+ /**
31
+ * Record a suggestion candidate. Idempotent on
32
+ * (entity_type, from_slug, into_slug, reason): re-recording a pending row
33
+ * refreshes its detail; a resolved (accepted/dismissed) row is NEVER
34
+ * resurrected — the sweep keeps re-detecting the same pairs and a user
35
+ * dismissal must stick.
36
+ */
37
+ async record(candidate) {
38
+ await this.db.query(`INSERT INTO entity_merge_suggestions (entity_type, from_slug, into_slug, reason, detail)
39
+ VALUES ($1, $2, $3, $4, $5::jsonb)
40
+ ON CONFLICT (entity_type, from_slug, into_slug, reason) DO UPDATE SET
41
+ detail = EXCLUDED.detail
42
+ WHERE entity_merge_suggestions.status = 'pending'`, [
43
+ candidate.entity_type,
44
+ candidate.from_slug,
45
+ candidate.into_slug,
46
+ candidate.reason,
47
+ candidate.detail ? JSON.stringify(candidate.detail) : null,
48
+ ]);
49
+ }
50
+ /** List pending suggestions, optionally filtered by entity type. */
51
+ async listPending(filter) {
52
+ const where = ["status = 'pending'"];
53
+ const params = [];
54
+ if (filter?.entityType) {
55
+ params.push(filter.entityType);
56
+ where.push(`entity_type = $${params.length}`);
57
+ }
58
+ const r = await this.db.query(`SELECT id, entity_type, from_slug, into_slug, reason, detail, status, created_at, resolved_at
59
+ FROM entity_merge_suggestions
60
+ WHERE ${where.join(" AND ")}
61
+ ORDER BY created_at, id`, params);
62
+ return r.rows.map(toRow);
63
+ }
64
+ /** Fetch a single suggestion by id. */
65
+ async get(id) {
66
+ const r = await this.db.query(`SELECT id, entity_type, from_slug, into_slug, reason, detail, status, created_at, resolved_at
67
+ FROM entity_merge_suggestions WHERE id = $1`, [id]);
68
+ return r.rows[0] ? toRow(r.rows[0]) : null;
69
+ }
70
+ /**
71
+ * Resolve a pending suggestion. `accepted` records the user's confirmation —
72
+ * the actual merge is executed separately via the explicit merge machinery.
73
+ */
74
+ async resolve(id, status) {
75
+ await this.db.query(`UPDATE entity_merge_suggestions
76
+ SET status = $1, resolved_at = NOW()
77
+ WHERE id = $2 AND status = 'pending'`, [status, id]);
78
+ }
79
+ }
80
+ //# sourceMappingURL=entity-suggestions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-suggestions.js","sourceRoot":"","sources":["../../src/store/entity-suggestions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AAsB5F,SAAS,KAAK,CAAC,CAAS;IACtB,OAAO;QACL,GAAG,CAAC;QACJ,MAAM,EACJ,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAA6B,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM;KAC9F,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,0BAA0B;IACjB;IAApB,YAAoB,EAAe;QAAf,OAAE,GAAF,EAAE,CAAa;IAAG,CAAC;IAEvC,0EAA0E;IAC1E,KAAK,CAAC,eAAe;QACnB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAC3B,0DAA0D,EAC1D,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CACzB,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,CAAC,SAAmC;QAC9C,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CACjB;;;;yDAImD,EACnD;YACE,SAAS,CAAC,WAAW;YACrB,SAAS,CAAC,SAAS;YACnB,SAAS,CAAC,SAAS;YACnB,SAAS,CAAC,MAAM;YAChB,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI;SAC3D,CACF,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,KAAK,CAAC,WAAW,CAAC,MAA0C;QAC1D,MAAM,KAAK,GAAG,CAAC,oBAAoB,CAAC,CAAC;QACrC,MAAM,MAAM,GAAc,EAAE,CAAC;QAC7B,IAAI,MAAM,EAAE,UAAU,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,kBAAkB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAC3B;;eAES,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;+BACH,EACzB,MAAM,CACP,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,uCAAuC;IACvC,KAAK,CAAC,GAAG,CAAC,EAAU;QAClB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CAC3B;mDAC6C,EAC7C,CAAC,EAAE,CAAC,CACL,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,EAAU,EAAE,MAAgC;QACxD,MAAM,IAAI,CAAC,EAAE,CAAC,KAAK,CACjB;;4CAEsC,EACtC,CAAC,MAAM,EAAE,EAAE,CAAC,CACb,CAAC;IACJ,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/store/migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAwGD,eAAO,MAAM,UAAU,EAAE,SAAS,EAOjC,CAAC;AAEF,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/store/migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAmMD,eAAO,MAAM,UAAU,EAAE,SAAS,EAUjC,CAAC;AAEF,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhE"}