@geraldmaron/construct 1.0.23 → 1.1.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 (106) hide show
  1. package/README.md +0 -2
  2. package/bin/construct +189 -207
  3. package/lib/agent-instructions/inject.mjs +25 -4
  4. package/lib/audit-rules.mjs +127 -0
  5. package/lib/audit-skills.mjs +43 -1
  6. package/lib/beads-client.mjs +9 -0
  7. package/lib/beads-optimistic.mjs +23 -71
  8. package/lib/bridges/copilot-proxy.mjs +116 -0
  9. package/lib/cli-commands.mjs +5 -1
  10. package/lib/comment-lint.mjs +1 -1
  11. package/lib/config/schema.mjs +1 -1
  12. package/lib/document-extract/docling-client.mjs +16 -6
  13. package/lib/document-extract/docling-sidecar.py +32 -2
  14. package/lib/document-extract.mjs +37 -10
  15. package/lib/document-ingest.mjs +90 -5
  16. package/lib/embed/inbox.mjs +6 -3
  17. package/lib/embed/recommendation-store.mjs +7 -289
  18. package/lib/embed/reconcile.mjs +2 -2
  19. package/lib/embed/roadmap.mjs +16 -1
  20. package/lib/engine/consolidate.mjs +160 -3
  21. package/lib/engine/contradiction-judge.mjs +71 -0
  22. package/lib/engine/contradiction.mjs +74 -0
  23. package/lib/hooks/config-protection.mjs +4 -4
  24. package/lib/hooks/session-reflect.mjs +5 -1
  25. package/lib/host-capabilities.mjs +30 -0
  26. package/lib/ingest/docling-remote.mjs +90 -0
  27. package/lib/ingest/strategy.mjs +1 -1
  28. package/lib/init-unified.mjs +9 -13
  29. package/lib/intake/git-queue.mjs +195 -0
  30. package/lib/intake/queue.mjs +9 -16
  31. package/lib/logging/rotate.mjs +18 -0
  32. package/lib/mcp/server.mjs +124 -12
  33. package/lib/mcp/tool-budget.mjs +53 -0
  34. package/lib/mcp/tools/storage.mjs +2 -3
  35. package/lib/mcp-catalog.json +4 -4
  36. package/lib/mcp-manager.mjs +59 -3
  37. package/lib/observation-store.mjs +38 -166
  38. package/lib/ollama/capability-store.mjs +78 -0
  39. package/lib/ollama/provision-context.mjs +344 -0
  40. package/lib/opencode-config.mjs +148 -0
  41. package/lib/opencode-telemetry.mjs +7 -0
  42. package/lib/orchestration/runtime.mjs +3 -2
  43. package/lib/orchestration-policy.mjs +41 -6
  44. package/lib/platforms/capabilities.mjs +100 -0
  45. package/lib/prompt-composer.js +12 -8
  46. package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
  47. package/lib/reconcile/index.mjs +0 -2
  48. package/lib/reflect/extractor.mjs +14 -1
  49. package/lib/reflect/salience.mjs +65 -0
  50. package/lib/rules-delivery.mjs +122 -0
  51. package/lib/runtime/uv-bootstrap.mjs +32 -17
  52. package/lib/service-manager.mjs +79 -259
  53. package/lib/setup.mjs +44 -425
  54. package/lib/specialists/prompt-schema.mjs +162 -0
  55. package/lib/specialists/scaffold.mjs +109 -0
  56. package/lib/status.mjs +3 -6
  57. package/lib/storage/admin.mjs +48 -325
  58. package/lib/storage/backend.mjs +10 -57
  59. package/lib/storage/embeddings-engine.mjs +19 -5
  60. package/lib/storage/hybrid-query.mjs +15 -196
  61. package/lib/storage/sync.mjs +36 -177
  62. package/lib/storage/vector-client.mjs +256 -235
  63. package/lib/strategy-store.mjs +35 -286
  64. package/lib/telemetry/beads-fallback.mjs +40 -0
  65. package/lib/telemetry/hook-calls.mjs +138 -0
  66. package/lib/worker/entrypoint.mjs +6 -14
  67. package/package.json +6 -5
  68. package/personas/construct.md +1 -1
  69. package/platforms/capabilities.json +76 -0
  70. package/platforms/claude/settings.template.json +0 -7
  71. package/platforms/opencode/sync-config.mjs +121 -25
  72. package/rules/common/neurodivergent-output.md +1 -1
  73. package/rules/web/coding-style.md +8 -0
  74. package/rules/web/design-quality.md +8 -0
  75. package/rules/web/hooks.md +8 -0
  76. package/rules/web/patterns.md +8 -0
  77. package/rules/web/performance.md +8 -0
  78. package/rules/web/security.md +8 -0
  79. package/rules/web/testing.md +8 -0
  80. package/scripts/sync-specialists.mjs +174 -40
  81. package/specialists/prompts/cx-architect.md +20 -0
  82. package/specialists/prompts/cx-qa.md +1 -1
  83. package/specialists/prompts/cx-test-automation.md +12 -0
  84. package/specialists/registry.json +0 -8
  85. package/templates/docs/construct_guide.md +1 -1
  86. package/db/schema/001_init.sql +0 -40
  87. package/db/schema/002_pgvector.sql +0 -182
  88. package/db/schema/003_intake.sql +0 -47
  89. package/db/schema/003_observation_reconciliation.sql +0 -14
  90. package/db/schema/004_recommendations.sql +0 -46
  91. package/db/schema/005_strategy.sql +0 -21
  92. package/db/schema/006_graph.sql +0 -24
  93. package/db/schema/007_tags.sql +0 -30
  94. package/db/schema/008_skill_usage.sql +0 -24
  95. package/db/schema/009_scheduler.sql +0 -14
  96. package/db/schema/010_cx_scores.sql +0 -51
  97. package/lib/intake/postgres-queue.mjs +0 -240
  98. package/lib/reconcile/postgres-namespace.mjs +0 -102
  99. package/lib/services/local-postgres.mjs +0 -15
  100. package/lib/storage/backup.mjs +0 -347
  101. package/lib/storage/migrations.mjs +0 -187
  102. package/lib/storage/postgres-backup.mjs +0 -124
  103. package/lib/storage/sql-store.mjs +0 -45
  104. package/lib/storage/store-version.mjs +0 -115
  105. package/lib/storage/unified-storage.mjs +0 -550
  106. package/lib/storage/vector-store.mjs +0 -100
@@ -1,202 +1,21 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * lib/storage/hybrid-query.mjs — hybrid file + SQL + semantic retrieval.
2
+ * lib/storage/hybrid-query.mjs — unified search across knowledge and facts.
3
+ *
4
+ * This is a stub for the new LanceDB-backed search.
4
5
  */
5
- import { loadStateSnapshot, summarizeStateSnapshot } from './state-source.mjs';
6
- import { describeSqlStore } from './sql-store.mjs';
7
- import { describeVectorStore, searchLocalVectorIndex, vectorSearchLocal } from './vector-store.mjs';
8
- import { createSqlClient, closeSqlClient, readVectorConfig } from './backend.mjs';
9
- import { embedText, getEmbeddingModelInfo } from './embeddings-engine.mjs';
10
- import { floatArrayToPgVector } from './vector-client.mjs';
11
- import { reciprocalRankFusion } from './rrf.mjs';
6
+ import { VectorClient } from './vector-client.mjs';
12
7
 
13
- // iterative_scan (pgvector >= 0.8.0) keeps a filtered ANN query from
14
- // under-returning; harmless for the current unfiltered query, and the correct
15
- // default once tag/metadata filters are added to the vector search.
8
+ export async function buildHybridSearchResultsAsync(rootDir, query, { limit = 10, env = process.env } = {}) {
9
+ // 1. Embed query
10
+ const { embedText } = await import('./embeddings-engine.mjs');
11
+ const { embedding } = await embedText(query, { env });
16
12
 
17
- async function supportsIterativeScan(client) {
18
- try {
19
- const [row] = await client`SELECT extversion AS v FROM pg_extension WHERE extname = 'vector'`;
20
- if (!row?.v) return false;
21
- const [major, minor] = String(row.v).split('.').map((n) => parseInt(n, 10));
22
- return major > 0 || (major === 0 && minor >= 8);
23
- } catch {
24
- return false;
25
- }
26
- }
27
-
28
- function collectFileCandidates(snapshot) {
29
- const docs = [];
30
-
31
- if (snapshot.context) {
32
- docs.push({
33
- id: '.cx/context.json',
34
- kind: 'context',
35
- title: 'Context state',
36
- summary: snapshot.context.contextSummary || snapshot.context.summary || null,
37
- body: JSON.stringify(snapshot.context, null, 2),
38
- tags: ['context', 'state', 'cx'],
39
- });
40
- }
41
-
42
- if (snapshot.architecture) {
43
- docs.push({
44
- id: 'docs/concepts/architecture.md',
45
- kind: 'architecture',
46
- title: 'Architecture docs',
47
- summary: snapshot.architecture.slice(0, 240),
48
- body: snapshot.architecture,
49
- tags: ['architecture', 'docs'],
50
- });
51
- }
52
-
53
- if (snapshot.docsReadme) {
54
- docs.push({
55
- id: 'docs/README.md',
56
- kind: 'docs',
57
- title: 'Docs index',
58
- summary: snapshot.docsReadme.slice(0, 240),
59
- body: snapshot.docsReadme,
60
- tags: ['docs', 'index'],
61
- });
62
- }
63
-
64
- for (const doc of snapshot.productIntelDocs ?? []) {
65
- const kind = doc.path.startsWith('docs/prd/')
66
- ? 'prd'
67
- : doc.path.startsWith('docs/meta-prd/')
68
- ? 'meta-prd'
69
- : 'knowledge';
70
- docs.push({
71
- id: doc.path,
72
- kind,
73
- title: doc.title,
74
- summary: doc.body.slice(0, 240),
75
- body: doc.body,
76
- tags: ['knowledge', kind],
77
- });
78
- }
79
-
80
- return docs;
81
- }
82
-
83
- export function buildHybridSearchResults(rootDir, query, { limit = 10, env = process.env } = {}) {
84
- const snapshot = loadStateSnapshot(rootDir);
85
- const fileCandidates = collectFileCandidates(snapshot);
86
- const sqlStore = describeSqlStore(env);
87
- const vectorStore = describeVectorStore(env);
88
- const fileHits = vectorSearchLocal(fileCandidates, query, { limit });
89
- const localVectorHits = vectorStore.mode === 'local' && vectorStore.indexPath
90
- ? searchLocalVectorIndex(vectorStore.indexPath, query, { limit })
91
- : [];
92
- const merged = [...fileHits];
93
- for (const hit of localVectorHits) {
94
- if (merged.some((entry) => entry.id === hit.id)) continue;
95
- merged.push({
96
- id: hit.id,
97
- kind: hit.kind,
98
- title: hit.title,
99
- summary: hit.summary,
100
- score: hit.score,
101
- source_path: hit.source_path,
102
- });
103
- }
104
-
105
- return {
106
- query,
107
- summary: summarizeStateSnapshot(snapshot),
108
- stores: {
109
- file: { configured: true, mode: 'canonical' },
110
- sql: sqlStore,
111
- vector: vectorStore,
112
- },
113
- results: merged.slice(0, limit).map((hit) => ({
114
- id: hit.id,
115
- kind: hit.kind,
116
- title: hit.title,
117
- summary: hit.summary,
118
- score: hit.score,
119
- })),
120
- };
121
- }
122
-
123
- export async function buildHybridSearchResultsAsync(rootDir, query, { limit = 10, env = process.env, embed = embedText, embeddingModel: modelOverride = null } = {}) {
124
- const base = buildHybridSearchResults(rootDir, query, { limit, env });
125
- const client = createSqlClient(env);
126
- if (!client) return base;
127
-
128
- // Resolve the active embedding model so the SQL filter and the query
129
- // embedding agree on dimensionality and identity.
130
- const embeddingModel = modelOverride || (await getEmbeddingModelInfo({ env })).model;
131
-
132
- try {
133
- const queryVec = floatArrayToPgVector((await embed(query, { env })).embedding);
134
- const iterative = await supportsIterativeScan(client);
135
-
136
- // Native pgvector ANN over the HNSW index (not a JS full scan). When the
137
- // extension supports it, relaxed_order iterative scan runs inside the txn.
138
- const runVector = (sql) => sql`
139
- select d.id, d.kind, d.title, d.summary, d.source_path
140
- from construct_documents d
141
- join construct_embeddings e on e.document_id = d.id
142
- where d.project = 'construct' and e.model = ${embeddingModel}
143
- order by e.embedding <=> ${queryVec}
144
- limit ${limit}
145
- `;
146
- const vectorHits = iterative
147
- ? await client.begin(async (sql) => {
148
- await sql`SET LOCAL hnsw.iterative_scan = relaxed_order`;
149
- return runVector(sql);
150
- })
151
- : await runVector(client);
152
-
153
- const keywordHits = await client`
154
- select id, kind, title, summary, source_path
155
- from construct_documents
156
- where project = 'construct'
157
- and (title ilike ${`%${query}%`} or coalesce(summary, '') ilike ${`%${query}%`} or body ilike ${`%${query}%`})
158
- order by updated_at desc
159
- limit ${limit}
160
- `;
161
-
162
- // One consolidated ranking: file BM25 (base) + neural ANN + keyword, fused
163
- // by Reciprocal Rank Fusion so three incompatible score scales merge by
164
- // rank rather than magnitude.
165
- const lists = [base.results, vectorHits, keywordHits];
166
- const byId = new Map();
167
- for (const list of lists) {
168
- for (const it of list) if (it && !byId.has(it.id)) byId.set(it.id, it);
169
- }
170
- const results = reciprocalRankFusion(lists, { idOf: (x) => x.id, limit }).map(({ id, score }) => {
171
- const it = byId.get(id);
172
- return { id, kind: it.kind, title: it.title, summary: it.summary, score, source_path: it.source_path ?? null };
173
- });
13
+ // 2. Search LanceDB
14
+ const client = new VectorClient({ env });
15
+ const docs = await client.searchDocuments({ project: 'construct', queryEmbedding: embedding, limit });
16
+ const obs = await client.searchObservations({ project: 'construct', queryEmbedding: embedding, limit });
174
17
 
175
- return {
176
- ...base,
177
- results,
178
- stores: {
179
- ...base.stores,
180
- vector: {
181
- ...base.stores.vector,
182
- ...readVectorConfig(env),
183
- model: embeddingModel,
184
- iterativeScan: iterative,
185
- },
186
- sql: {
187
- ...base.stores.sql,
188
- mode: 'postgres',
189
- configured: true,
190
- sharedReady: true,
191
- },
192
- },
193
- };
194
- } catch (error) {
195
- return {
196
- ...base,
197
- error: error?.message || 'hybrid search failed',
198
- };
199
- } finally {
200
- await closeSqlClient(client);
201
- }
18
+ // 3. Merge and rank (simple merge for now)
19
+ const all = [...docs.map(d => ({ ...d, type: 'document' })), ...obs.map(o => ({ ...o, type: 'observation' }))];
20
+ return all.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
202
21
  }
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/storage/sync.mjs — sync file-state artifacts into shared Postgres indices.
3
+ * lib/storage/sync.mjs — sync file-state artifacts into local LanceDB indices.
4
+ *
5
+ * This implementation replaces the Postgres-backed sync with a LanceDB-backed
6
+ * one, matching the new local-first, Git-backed architecture.
4
7
  */
5
8
  import crypto from 'node:crypto';
6
9
  import { loadStateSnapshot } from './state-source.mjs';
7
- import { createSqlClient, closeSqlClient, readVectorConfig } from './backend.mjs';
8
10
  import { embedBatch, getEmbeddingModelInfo } from './embeddings-engine.mjs';
9
- import { writeLocalVectorIndex } from './vector-store.mjs';
10
- import { runMigrations } from './migrations.mjs';
11
- import { isManagedConstructPostgresUrl } from '../service-manager.mjs';
12
- import { floatArrayToPgVector } from './vector-client.mjs';
11
+ import { VectorClient } from './vector-client.mjs';
13
12
 
14
13
  function hashContent(content) {
15
14
  return crypto.createHash('sha256').update(String(content || '')).digest('hex');
@@ -25,7 +24,7 @@ function toDocumentRows(rootDir, snapshot, project = 'construct') {
25
24
  project,
26
25
  kind: 'context',
27
26
  title: 'Context state',
28
- summary: snapshot.context.contextSummary || snapshot.context.summary || null,
27
+ summary: snapshot.context.contextSummary || snapshot.context.summary || '',
29
28
  body,
30
29
  source_path: '.cx/context.json',
31
30
  tags: ['context', 'state', 'cx'],
@@ -87,187 +86,47 @@ function toDocumentRows(rootDir, snapshot, project = 'construct') {
87
86
  return rows;
88
87
  }
89
88
 
90
- /**
91
- * Ensure the hybrid storage schema is current by running every pending
92
- * migration in order. Uses `lib/storage/migrations.mjs` which tracks
93
- * applied state in `construct_schema_migrations` and surfaces drift on
94
- * changed files.
95
- */
96
- export async function ensureHybridSchema(client) {
97
- await runMigrations(client);
98
- }
99
-
100
89
  export async function syncFileStateToSql(rootDir, { env = process.env, project = 'construct' } = {}) {
101
90
  const snapshot = loadStateSnapshot(rootDir);
102
91
  const rows = toDocumentRows(rootDir, snapshot, project);
103
- const vectorConfig = readVectorConfig(env);
104
-
105
- // Resolve the active embedding model once per sync so every record carries
106
- // a consistent model id and dimensionality.
92
+
107
93
  const modelInfo = await getEmbeddingModelInfo({ env });
108
94
  const embeddingModel = modelInfo.model;
109
95
 
110
- // Pre-flight check: verify database is reachable before attempting sync
111
- const probeClient = createSqlClient(env);
112
- if (probeClient) {
113
- try {
114
- await probeClient`SELECT 1`;
115
- } catch (error) {
116
- // Database unreachable - check if it's a managed Postgres that needs startup
117
- const databaseUrl = env.DATABASE_URL || '';
118
- await closeSqlClient(probeClient);
119
-
120
- if (isManagedConstructPostgresUrl(databaseUrl)) {
121
- return {
122
- status: 'degraded',
123
- error: 'Managed Postgres is not running. Run `construct up` to start services, then retry.',
124
- documentsSynced: 0,
125
- embeddingsSynced: 0,
126
- embeddingModel,
127
- vector: vectorConfig,
128
- localVector: { status: 'unavailable', note: 'Sync failed - database offline' },
129
- sql: { status: 'unavailable', error: 'ECONNREFUSED - Postgres not running' },
130
- hint: 'Run `construct up` to start managed Postgres and other runtime services',
131
- };
132
- }
133
-
134
- return {
135
- status: 'degraded',
136
- error: `Database connection failed: ${error.message}`,
137
- documentsSynced: 0,
138
- embeddingsSynced: 0,
139
- embeddingModel,
140
- vector: vectorConfig,
141
- localVector: { status: 'unavailable', note: 'Sync failed - database offline' },
142
- sql: { status: 'unavailable', error: error.message },
143
- };
144
- }
145
- await closeSqlClient(probeClient);
146
- }
147
-
148
- // Batch-embed all documents through the engine. The engine selects the
149
- // configured adapter (local ONNX, openai, ollama, hashing) and returns
150
- // Float32Array embeddings of `modelInfo.dimensions` length.
151
- const embedTexts = rows.map((row) =>
152
- [row.title, row.summary, row.body, row.source_path, row.kind].filter(Boolean).join('\n')
153
- );
154
- const embeddings = embedTexts.length > 0 ? await embedBatch(embedTexts, { env }) : [];
155
-
156
- const localVectorRecords = rows.map((row, i) => ({
157
- id: row.id,
158
- project: row.project,
159
- kind: row.kind,
160
- title: row.title,
161
- summary: row.summary,
162
- body: row.body,
163
- source_path: row.source_path,
164
- tags: row.tags,
165
- content_hash: row.content_hash,
166
- embedding: Array.from(embeddings[i].embedding).map((v) => Number(v)),
167
- updatedAt: new Date().toISOString(),
168
- }));
169
-
170
- let localVector = { status: 'unavailable', note: 'No local vector index configured', recordsSynced: 0 };
171
- if (vectorConfig.indexPath) {
172
- const payload = writeLocalVectorIndex(vectorConfig.indexPath, localVectorRecords, { model: embeddingModel });
173
- localVector = {
174
- status: 'ok',
175
- indexPath: vectorConfig.indexPath,
176
- model: payload.model,
177
- recordsSynced: payload.records.length,
178
- updatedAt: payload.updatedAt,
179
- };
180
- }
181
-
182
- if (!probeClient) {
96
+ const vectorClient = new VectorClient({ env });
97
+
98
+ if (rows.length === 0) {
183
99
  return {
184
- status: localVector.status === 'ok' ? 'ok' : 'unavailable',
185
- note: 'No DATABASE_URL configured',
186
- documentsSynced: rows.length,
187
- embeddingsSynced: localVector.recordsSynced,
100
+ status: 'ok',
101
+ documentsSynced: 0,
102
+ embeddingsSynced: 0,
188
103
  embeddingModel,
189
- vector: vectorConfig,
190
- localVector,
191
- sql: { status: 'unavailable', note: 'No DATABASE_URL configured' },
104
+ backend: 'lancedb'
192
105
  };
193
106
  }
194
107
 
195
- // Create a fresh client for the actual sync operation
196
- const syncClient = createSqlClient(env);
197
- if (!syncClient) {
198
- return {
199
- status: localVector.status === 'ok' ? 'ok' : 'unavailable',
200
- note: 'No DATABASE_URL configured',
201
- documentsSynced: rows.length,
202
- embeddingsSynced: localVector.recordsSynced,
203
- embeddingModel,
204
- vector: vectorConfig,
205
- localVector,
206
- sql: { status: 'unavailable', note: 'No DATABASE_URL configured' },
207
- };
108
+ // Batch-embed all documents
109
+ const embedTexts = rows.map((row) =>
110
+ [row.title, row.summary, row.body, row.source_path, row.kind].filter(Boolean).join('\n')
111
+ );
112
+ const embeddings = await embedBatch(embedTexts, { env });
113
+
114
+ let documentsSynced = 0;
115
+ for (let i = 0; i < rows.length; i++) {
116
+ const row = rows[i];
117
+ await vectorClient.storeDocument({
118
+ ...row,
119
+ embedding: embeddings[i].embedding,
120
+ model: embeddingModel
121
+ });
122
+ documentsSynced += 1;
208
123
  }
209
124
 
210
- try {
211
- await ensureHybridSchema(syncClient);
212
-
213
- let documentsSynced = 0;
214
- let embeddingsSynced = 0;
215
- for (const row of rows) {
216
- await syncClient`
217
- insert into construct_documents (id, project, kind, title, summary, body, source_path, tags, content_hash, updated_at)
218
- values (${row.id}, ${row.project}, ${row.kind}, ${row.title}, ${row.summary}, ${row.body}, ${row.source_path}, ${JSON.stringify(row.tags)}, ${row.content_hash}, now())
219
- on conflict (id) do update set
220
- project = excluded.project,
221
- kind = excluded.kind,
222
- title = excluded.title,
223
- summary = excluded.summary,
224
- body = excluded.body,
225
- source_path = excluded.source_path,
226
- tags = excluded.tags,
227
- content_hash = excluded.content_hash,
228
- updated_at = now()
229
- `;
230
- documentsSynced += 1;
231
-
232
- const embeddingVec = floatArrayToPgVector(localVectorRecords[documentsSynced - 1].embedding);
233
- await syncClient`
234
- insert into construct_embeddings (document_id, model, embedding, content_hash, updated_at)
235
- values (${row.id}, ${embeddingModel}, ${embeddingVec}, ${row.content_hash}, now())
236
- on conflict (document_id) do update set
237
- model = excluded.model,
238
- embedding = excluded.embedding,
239
- content_hash = excluded.content_hash,
240
- updated_at = now()
241
- `;
242
- embeddingsSynced += 1;
243
- }
244
-
245
- await syncClient`
246
- insert into construct_sync_runs (project, source, documents_synced, embeddings_synced, status, note)
247
- values (${project}, ${'file-state'}, ${documentsSynced}, ${0}, ${'ok'}, ${'synced file-state documents'})
248
- `;
249
-
250
- return {
251
- status: 'ok',
252
- documentsSynced,
253
- embeddingsSynced,
254
- embeddingModel,
255
- vector: vectorConfig,
256
- localVector,
257
- sql: { status: 'ok' },
258
- };
259
- } catch (error) {
260
- return {
261
- status: localVector.status === 'ok' ? 'degraded' : 'degraded',
262
- error: error?.message || 'sync failed',
263
- documentsSynced: rows.length,
264
- embeddingsSynced: localVector.recordsSynced,
265
- embeddingModel,
266
- vector: vectorConfig,
267
- localVector,
268
- sql: { status: 'degraded', error: error?.message || 'sync failed' },
269
- };
270
- } finally {
271
- await closeSqlClient(syncClient);
272
- }
125
+ return {
126
+ status: 'ok',
127
+ documentsSynced,
128
+ embeddingsSynced: documentsSynced,
129
+ embeddingModel,
130
+ backend: 'lancedb'
131
+ };
273
132
  }