@geraldmaron/construct 1.0.23 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/bin/construct +15 -215
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/reconcile/index.mjs +0 -2
- package/lib/service-manager.mjs +38 -256
- package/lib/setup.mjs +26 -426
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/platforms/claude/settings.template.json +0 -7
- package/scripts/sync-specialists.mjs +46 -12
- package/specialists/prompts/cx-qa.md +1 -1
- package/specialists/registry.json +0 -8
- package/templates/docs/construct_guide.md +1 -1
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/reconcile/postgres-namespace.mjs +0 -102
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/unified-storage.mjs +0 -550
- 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 —
|
|
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 {
|
|
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
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
}
|
package/lib/storage/sync.mjs
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/storage/sync.mjs — sync file-state artifacts into
|
|
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 {
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
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:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
embeddingsSynced: localVector.recordsSynced,
|
|
100
|
+
status: 'ok',
|
|
101
|
+
documentsSynced: 0,
|
|
102
|
+
embeddingsSynced: 0,
|
|
188
103
|
embeddingModel,
|
|
189
|
-
|
|
190
|
-
localVector,
|
|
191
|
-
sql: { status: 'unavailable', note: 'No DATABASE_URL configured' },
|
|
104
|
+
backend: 'lancedb'
|
|
192
105
|
};
|
|
193
106
|
}
|
|
194
107
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
}
|