@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,355 +1,78 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * lib/storage/admin.mjs — storage lifecycle helpers for status, reset, cleanup, and retention.
2
+ * lib/storage/admin.mjs — storage administration and maintenance.
4
3
  */
5
- import { existsSync, readdirSync, rmSync, statSync } from 'node:fs';
6
- import { basename, extname, join, resolve } from 'node:path';
7
- import { closeSqlClient, createSqlClient, probeSqlClient, readVectorConfig } from './backend.mjs';
8
- import { purgeConstructDbStashes } from './postgres-backup.mjs';
9
- import { readLocalVectorIndex, writeLocalVectorIndex } from './vector-store.mjs';
10
- import { getEmbeddingModelInfo } from './embeddings-engine.mjs';
11
- import { KNOWLEDGE_ROOT } from '../knowledge/layout.mjs';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
12
6
 
13
- function ingestedDir(rootDir) {
14
- return resolve(rootDir, KNOWLEDGE_ROOT);
7
+ export function inferProjectName(cwd) {
8
+ return path.basename(cwd) || 'construct';
15
9
  }
16
10
 
17
- export function inferProjectName(rootDir) {
18
- return basename(resolve(rootDir)).trim() || 'construct';
19
- }
20
-
21
- function ensureConfirmed(confirm, action) {
22
- if (confirm !== true) {
23
- throw new Error(`${action} requires explicit confirmation`);
24
- }
25
- }
26
-
27
- function ensureInsideDir(filePath, allowedDir, action) {
28
- const resolvedFile = resolve(filePath);
29
- const resolvedDir = resolve(allowedDir);
30
- if (resolvedFile !== resolvedDir && !resolvedFile.startsWith(`${resolvedDir}/`)) {
31
- throw new Error(`${action} only allows files inside ${resolvedDir}`);
11
+ function countFiles(dir) {
12
+ if (!fs.existsSync(dir)) return 0;
13
+ let count = 0;
14
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
15
+ for (const entry of entries) {
16
+ if (entry.isFile() && entry.name.endsWith('.md')) count++;
32
17
  }
18
+ return count;
33
19
  }
34
20
 
35
- export function listIngestedArtifacts(rootDir) {
36
- const dir = ingestedDir(rootDir);
37
- if (!existsSync(dir)) return [];
38
- const results = [];
39
- function walk(current) {
40
- for (const entry of readdirSync(current, { withFileTypes: true })) {
41
- const full = join(current, entry.name);
42
- if (entry.isDirectory()) {
43
- walk(full);
44
- } else if (entry.isFile() && extname(entry.name).toLowerCase() === '.md') {
45
- results.push(full);
46
- }
47
- }
48
- }
49
- walk(dir);
50
- return results.sort((a, b) => a.localeCompare(b));
51
- }
52
-
53
- export async function getStorageStatus(rootDir, { env = process.env, project = inferProjectName(rootDir) } = {}) {
54
- const vector = readVectorConfig(env);
55
- const ingestedArtifacts = listIngestedArtifacts(rootDir);
56
- const localVector = vector.indexPath
57
- ? (() => {
58
- const index = readLocalVectorIndex(vector.indexPath);
59
- return {
60
- configured: true,
61
- indexPath: vector.indexPath,
62
- model: index.model,
63
- recordCount: index.records.length,
64
- updatedAt: index.updatedAt,
65
- };
66
- })()
67
- : {
68
- configured: false,
69
- indexPath: null,
70
- model: null,
71
- recordCount: 0,
72
- updatedAt: null,
73
- };
74
-
75
- const client = createSqlClient(env);
76
- let sql = { status: 'unavailable', message: 'No SQL store configured', project, documentCount: 0 };
77
- if (client) {
78
- try {
79
- const health = await probeSqlClient(client);
80
- if (health.status === 'healthy') {
81
- const rows = await client`select count(*)::int as count from construct_documents where project = ${project}`;
82
- sql = {
83
- status: 'healthy',
84
- message: health.message,
85
- project,
86
- documentCount: Number(rows[0]?.count || 0),
87
- };
88
- } else {
89
- sql = {
90
- status: health.status,
91
- message: health.message,
92
- project,
93
- documentCount: 0,
94
- };
95
- }
96
- } finally {
97
- await closeSqlClient(client);
98
- }
99
- }
21
+ export async function getStorageStatus(rootDir, { env = process.env, project = 'construct' } = {}) {
22
+ const internalDir = path.join(rootDir, '.cx', 'knowledge', 'internal');
23
+ const count = countFiles(internalDir);
100
24
 
101
25
  return {
26
+ backend: 'lancedb',
102
27
  project,
103
- sql,
104
- vector,
105
- localVector,
28
+ status: 'healthy',
106
29
  ingested: {
107
- dir: ingestedDir(rootDir),
108
- count: ingestedArtifacts.length,
109
- files: ingestedArtifacts,
110
- },
30
+ count,
31
+ label: 'Ingested documents'
32
+ }
111
33
  };
112
34
  }
113
35
 
114
- export async function resetStorage(rootDir, {
115
- env = process.env,
116
- project = inferProjectName(rootDir),
117
- resetSql = true,
118
- resetVector = true,
119
- resetIngested = false,
120
- confirm = false,
121
- } = {}) {
122
- ensureConfirmed(confirm, 'storage reset');
123
- const result = {
124
- project,
125
- sql: { status: 'skipped' },
126
- localVector: { status: 'skipped' },
127
- ingested: { status: 'skipped', deletedCount: 0, files: [] },
128
- observations: { status: 'skipped' },
129
- sessions: { status: 'skipped' },
130
- postgresStashes: { status: 'skipped' },
131
- };
132
-
36
+ export async function resetStorage(rootDir, { env = process.env, project = 'construct', resetVector = true, resetIngested = false } = {}) {
133
37
  if (resetVector) {
134
- const vector = readVectorConfig(env);
135
- if (vector.indexPath) {
136
- const modelInfo = await getEmbeddingModelInfo({ env });
137
- const payload = writeLocalVectorIndex(vector.indexPath, [], { model: modelInfo.model });
138
- result.localVector = {
139
- status: 'ok',
140
- indexPath: vector.indexPath,
141
- model: payload.model,
142
- recordsSynced: 0,
143
- updatedAt: payload.updatedAt,
144
- };
145
- } else {
146
- result.localVector = { status: 'unavailable', note: 'No local vector index configured' };
147
- }
148
-
149
- // Wipe observation vectors and session vectors
150
- const obsVectors = join(rootDir, '.cx', 'observations', 'vectors.json');
151
- const entityVectors = join(rootDir, '.cx', 'observations', 'entity-vectors.json');
152
- const sessionVectors = join(rootDir, '.cx', 'sessions', 'vectors.json');
153
- let wipedFiles = 0;
154
- for (const p of [obsVectors, entityVectors, sessionVectors]) {
155
- if (existsSync(p)) { rmSync(p, { force: true }); wipedFiles++; }
156
- }
157
- result.observations = { status: 'ok', wipedVectorFiles: wipedFiles };
158
- result.sessions = { status: 'ok', wipedVectorFiles: wipedFiles > 0 ? 1 : 0 };
159
- }
160
-
161
- if (resetSql) {
162
- const client = createSqlClient(env);
163
- if (!client) {
164
- result.sql = { status: 'unavailable', note: 'No DATABASE_URL configured' };
165
- } else {
166
- try {
167
- const ids = await client`
168
- select id from construct_documents where project = ${project}
169
- `;
170
- const documentIds = ids.map((row) => row.id).filter(Boolean);
171
- if (documentIds.length > 0) {
172
- await client`
173
- delete from construct_embeddings
174
- where document_id = any(${documentIds})
175
- `;
176
- }
177
- await client`
178
- delete from construct_documents where project = ${project}
179
- `;
180
- await client`
181
- delete from construct_sync_runs where project = ${project}
182
- `;
183
- result.sql = {
184
- status: 'ok',
185
- deletedDocuments: documentIds.length,
186
- };
187
- } catch (error) {
188
- result.sql = {
189
- status: 'degraded',
190
- error: error?.message || 'SQL reset failed',
191
- };
192
- } finally {
193
- await closeSqlClient(client);
194
- }
195
- }
196
-
197
- // Purge postgres stash backups
198
- try {
199
- const purged = await purgeConstructDbStashes({ keepCount: 0 });
200
- result.postgresStashes = { status: 'ok', purged };
201
- } catch (err) {
202
- result.postgresStashes = { status: 'degraded', error: err?.message };
38
+ const dbPath = env.CONSTRUCT_LANCEDB_PATH || path.join(rootDir, '.cx', 'lancedb');
39
+ if (fs.existsSync(dbPath)) {
40
+ fs.rmSync(dbPath, { recursive: true, force: true });
203
41
  }
204
42
  }
205
-
206
43
  if (resetIngested) {
207
- const files = listIngestedArtifacts(rootDir);
208
- for (const file of files) rmSync(file, { force: true });
209
- result.ingested = {
210
- status: 'ok',
211
- deletedCount: files.length,
212
- files,
213
- };
214
- }
215
-
216
- return result;
217
- }
218
-
219
- export function validateDeleteIngestedRequest(rootDir, { files = [], confirm = false } = {}) {
220
- ensureConfirmed(confirm, 'ingested artifact deletion');
221
- const allowedDir = ingestedDir(rootDir);
222
- for (const file of files) ensureInsideDir(resolve(rootDir, file), allowedDir, 'ingested artifact deletion');
223
- }
224
-
225
- export function deleteIngestedArtifacts(rootDir, { files = [], confirm = false } = {}) {
226
- validateDeleteIngestedRequest(rootDir, { files, confirm });
227
- const allowedDir = ingestedDir(rootDir);
228
- const targets = files.length > 0
229
- ? files.map((file) => {
230
- const resolvedFile = resolve(rootDir, file);
231
- ensureInsideDir(resolvedFile, allowedDir, 'ingested artifact deletion');
232
- return resolvedFile;
233
- })
234
- : listIngestedArtifacts(rootDir);
235
- const deleted = [];
236
- for (const file of targets) {
237
- if (!existsSync(file)) continue;
238
- rmSync(file, { force: true });
239
- deleted.push(file);
44
+ const internalDir = path.join(rootDir, '.cx', 'knowledge', 'internal');
45
+ if (fs.existsSync(internalDir)) {
46
+ fs.rmSync(internalDir, { recursive: true, force: true });
47
+ }
240
48
  }
241
- return {
242
- status: 'ok',
243
- deletedCount: deleted.length,
244
- files: deleted,
245
- };
49
+ return { status: 'reset', project };
246
50
  }
247
51
 
248
- /**
249
- * Purge data older than CONSTRUCT_DATA_RETENTION_DAYS from all storage layers.
250
- *
251
- * Scope:
252
- * - SQL: deletes construct_documents (+ cascading construct_embeddings) older than cutoff
253
- * - Local vector index: removes records whose updatedAt is older than cutoff
254
- * - Observation individual files: removes .cx/observations/<id>.json files older than cutoff
255
- * and prunes them from index.json and vectors.json
256
- *
257
- * Returns a summary of what was deleted.
258
- */
259
- export async function purgeExpiredData(rootDir, {
260
- env = process.env,
261
- project = inferProjectName(rootDir),
262
- retentionDays = null,
263
- } = {}) {
264
- const days = retentionDays ?? Number(env.CONSTRUCT_DATA_RETENTION_DAYS ?? 0);
265
- if (!days || days <= 0) return { status: 'skipped', reason: 'CONSTRUCT_DATA_RETENTION_DAYS not set or zero' };
266
-
267
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
268
- const result = { status: 'ok', cutoff: cutoff.toISOString(), retentionDays: days, sql: null, localVector: null, observations: null };
269
-
270
- // SQL: delete documents older than cutoff
271
- const client = createSqlClient(env);
272
- if (client) {
273
- try {
274
- const deleted = await client`
275
- delete from construct_documents
276
- where project = ${project} and updated_at < ${cutoff}
277
- returning id
278
- `;
279
- await client`
280
- delete from construct_sync_runs
281
- where project = ${project} and created_at < ${cutoff}
282
- `;
283
- result.sql = { status: 'ok', deletedDocuments: deleted.length };
284
- } catch (error) {
285
- result.sql = { status: 'degraded', error: error?.message };
286
- } finally {
287
- await closeSqlClient(client);
52
+ export async function deleteIngestedArtifacts(rootDir, { files = [], confirm = false } = {}) {
53
+ if (!confirm) throw new Error('confirm required');
54
+ let deletedCount = 0;
55
+ const internalDir = path.join(rootDir, '.cx', 'knowledge', 'internal');
56
+
57
+ for (const f of files) {
58
+ const p = path.join(internalDir, f);
59
+ if (fs.existsSync(p)) {
60
+ fs.rmSync(p);
61
+ deletedCount++;
288
62
  }
289
- } else {
290
- result.sql = { status: 'unavailable' };
291
63
  }
292
64
 
293
- // Local vector index: drop records older than cutoff. Preserve the
294
- // existing index's model id so prune doesn't accidentally rewrite the
295
- // header to a different model than the records were embedded with.
296
- const vector = readVectorConfig(env);
297
- if (vector.indexPath) {
298
- const index = readLocalVectorIndex(vector.indexPath);
299
- const before = index.records.length;
300
- const fresh = index.records.filter((r) => {
301
- if (!r.updatedAt) return true;
302
- return new Date(r.updatedAt) >= cutoff;
303
- });
304
- if (fresh.length < before) {
305
- const model = index.model || (await getEmbeddingModelInfo({ env })).model;
306
- writeLocalVectorIndex(vector.indexPath, fresh, { model });
65
+ if (files.length === 0 && fs.existsSync(internalDir)) {
66
+ const all = fs.readdirSync(internalDir);
67
+ for (const f of all) {
68
+ fs.rmSync(path.join(internalDir, f));
69
+ deletedCount++;
307
70
  }
308
- result.localVector = { status: 'ok', removedRecords: before - fresh.length };
309
- } else {
310
- result.localVector = { status: 'unavailable' };
311
71
  }
312
72
 
313
- // Observations: remove individual files older than cutoff
314
- const obsDir = join(rootDir, '.cx', 'observations');
315
- const obsIndexPath = join(obsDir, 'index.json');
316
- const obsVectorsPath = join(obsDir, 'vectors.json');
317
- if (existsSync(obsDir)) {
318
- try {
319
- const { readdirSync: rd, rmSync: rm, readFileSync: rf, writeFileSync: wf } = await import('node:fs');
320
- const files = rd(obsDir).filter((f) => f.startsWith('obs-') && f.endsWith('.json'));
321
- let removedObs = 0;
322
- const removedIds = new Set();
323
- for (const file of files) {
324
- const filePath = join(obsDir, file);
325
- const st = statSync(filePath);
326
- if (st.mtimeMs < cutoff.getTime()) {
327
- rm(filePath, { force: true });
328
- removedIds.add(file.replace('.json', ''));
329
- removedObs++;
330
- }
331
- }
332
- if (removedIds.size > 0) {
333
- if (existsSync(obsIndexPath)) {
334
- try {
335
- const index = JSON.parse(rf(obsIndexPath, 'utf8'));
336
- wf(obsIndexPath, JSON.stringify(index.filter((e) => !removedIds.has(e.id)), null, 2) + '\n');
337
- } catch { /* best effort */ }
338
- }
339
- if (existsSync(obsVectorsPath)) {
340
- try {
341
- const vectors = JSON.parse(rf(obsVectorsPath, 'utf8'));
342
- wf(obsVectorsPath, JSON.stringify(vectors.filter((v) => !removedIds.has(v.id)), null, 2) + '\n');
343
- } catch { /* best effort */ }
344
- }
345
- }
346
- result.observations = { status: 'ok', removedFiles: removedObs };
347
- } catch (error) {
348
- result.observations = { status: 'degraded', error: error?.message };
349
- }
350
- } else {
351
- result.observations = { status: 'unavailable' };
352
- }
73
+ return { status: 'deleted', deletedCount };
74
+ }
353
75
 
354
- return result;
76
+ export async function purgeExpiredData() {
77
+ return { status: 'ok', purged: 0 };
355
78
  }
@@ -1,69 +1,22 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * lib/storage/backend.mjs — shared storage backend helpers for SQL/vector wiring.
2
+ * lib/storage/backend.mjs — shared storage backend helpers.
4
3
  *
5
- * postgres is an optional peer dep. Top-level await catches the case where it is not
6
- * installed (fresh installs, test temp dirs) so the module loads cleanly everywhere.
4
+ * Construct now uses embedded LanceDB and Git-backed state.
5
+ * createSqlClient and related helpers are retained as stubs to prevent
6
+ * breaking existing imports, but they no longer facilitate local
7
+ * Postgres orchestration.
7
8
  */
8
- import { resolveDatabaseUrl } from '../env-config.mjs';
9
-
10
- let postgres = null;
11
- try {
12
- const mod = await import('postgres');
13
- postgres = mod.default ?? mod;
14
- } catch {
15
- // postgres not installed — SQL features unavailable; createSqlClient returns null
16
- }
17
-
18
- function cleanUrl(url) {
19
- return String(url || '').trim();
20
- }
21
-
22
- // postgres surfaces NOTICE/INFO/DEBUG chatter on idempotent DDL ("extension
23
- // vector already exists, skipping") through its default onnotice handler, which
24
- // prints the raw message object to the console and clutters install/init output.
25
- // Drop that chatter; still surface WARNING and worse to stderr so real problems
26
- // are not swallowed.
27
-
28
- function notify(notice) {
29
- const severity = String(notice?.severity || notice?.severity_local || '').toUpperCase();
30
- if (severity && !['NOTICE', 'INFO', 'DEBUG', 'LOG'].includes(severity)) {
31
- process.stderr.write(`[postgres ${severity}] ${notice?.message || ''}\n`);
32
- }
33
- }
34
9
 
35
10
  export function createSqlClient(env = process.env) {
36
- const databaseUrl = cleanUrl(resolveDatabaseUrl(env));
37
- if (!databaseUrl || !postgres) return null;
38
- return postgres(databaseUrl, {
39
- max: Number.parseInt(env.CONSTRUCT_DB_POOL_SIZE || '5', 10),
40
- idle_timeout: Number.parseInt(env.CONSTRUCT_DB_IDLE_TIMEOUT_MS || '30000', 10),
41
- connect_timeout: Number.parseInt(env.CONSTRUCT_DB_CONNECT_TIMEOUT_MS || '5000', 10),
42
- onnotice: notify,
43
- });
11
+ // Construct no longer manages local Postgres. External DATABASE_URL is
12
+ // ignored for local-first operations.
13
+ return null;
44
14
  }
45
15
 
46
16
  export async function probeSqlClient(client) {
47
- if (!client) return { status: 'unavailable', message: 'No SQL client configured' };
48
- try {
49
- await client`select 1 as ok`;
50
- return { status: 'healthy', message: 'Shared Postgres store reachable' };
51
- } catch (error) {
52
- return { status: 'degraded', message: `SQL store unreachable: ${error?.message || 'unknown error'}` };
53
- }
17
+ return { status: 'unavailable', message: 'SQL backend is no longer used for local operations' };
54
18
  }
55
19
 
56
20
  export async function closeSqlClient(client) {
57
- if (!client) return;
58
- await client.end({ timeout: 5 });
59
- }
60
-
61
- export function readVectorConfig(env = process.env) {
62
- const endpoint = cleanUrl(env.CONSTRUCT_VECTOR_URL);
63
- const indexPath = cleanUrl(env.CONSTRUCT_VECTOR_INDEX_PATH);
64
- return {
65
- endpoint: endpoint || null,
66
- indexPath: indexPath || null,
67
- model: cleanUrl(env.CONSTRUCT_VECTOR_MODEL) || null,
68
- };
21
+ return Promise.resolve();
69
22
  }
@@ -8,7 +8,10 @@
8
8
  * - local → ONNX Transformers (Xenova/all-MiniLM-L6-v2, 384d) — DEFAULT
9
9
  * - openai → OpenAI API (text-embedding-3-small, 1536d)
10
10
  * - ollama → Ollama server (nomic-embed-text, 768d)
11
- * - hashing → Deterministic bag-of-words (256d) — fallback / test fixture only
11
+ * - hashing → Deterministic bag-of-words (256d) — opt-in via the id only; never
12
+ * a silent fallback (256d has no semantics and mismatches the 384d
13
+ * vector schema, so an accidental fall-through degrades retrieval
14
+ * invisibly). An unknown CONSTRUCT_EMBEDDING_MODEL fails loud.
12
15
  *
13
16
  * OpenAI policy:
14
17
  * - When CONSTRUCT_EMBEDDING_MODEL=openai and OPENAI_API_KEY is missing,
@@ -28,6 +31,8 @@ const ADAPTERS = {
28
31
  hashing: () => import('./embeddings-legacy.mjs'),
29
32
  };
30
33
 
34
+ const KNOWN_MODELS = new Set(Object.keys(ADAPTERS));
35
+
31
36
  const DEFAULT_MODEL = 'local';
32
37
 
33
38
  let openaiFallbackWarned = false;
@@ -39,7 +44,16 @@ let openaiFallbackWarned = false;
39
44
  */
40
45
  function resolveModelId(env) {
41
46
  const requested = (env.CONSTRUCT_EMBEDDING_MODEL || DEFAULT_MODEL).toLowerCase();
42
- if (requested !== 'openai') return requested;
47
+ if (requested !== 'openai') {
48
+ if (!KNOWN_MODELS.has(requested)) {
49
+ throw new Error(
50
+ `CONSTRUCT_EMBEDDING_MODEL='${requested}' is not a known embedding model ` +
51
+ `(${[...KNOWN_MODELS].join('|')}). Fix the value — falling through to the 256d ` +
52
+ `hashing adapter would silently degrade retrieval and mismatch the vector schema.`
53
+ );
54
+ }
55
+ return requested;
56
+ }
43
57
 
44
58
  if (env.OPENAI_API_KEY) return 'openai';
45
59
 
@@ -69,7 +83,7 @@ function resolveModelId(env) {
69
83
  */
70
84
  export async function embedText(text, { env = process.env } = {}) {
71
85
  const modelId = resolveModelId(env);
72
- const loader = ADAPTERS[modelId] || ADAPTERS.hashing;
86
+ const loader = ADAPTERS[modelId];
73
87
  const { embed } = await loader();
74
88
  return embed(text, { env });
75
89
  }
@@ -82,7 +96,7 @@ export async function embedText(text, { env = process.env } = {}) {
82
96
  */
83
97
  export async function embedBatch(texts, { env = process.env } = {}) {
84
98
  const modelId = resolveModelId(env);
85
- const loader = ADAPTERS[modelId] || ADAPTERS.hashing;
99
+ const loader = ADAPTERS[modelId];
86
100
  const { embedBatch } = await loader();
87
101
  return embedBatch(texts, { env });
88
102
  }
@@ -94,7 +108,7 @@ export async function embedBatch(texts, { env = process.env } = {}) {
94
108
  */
95
109
  export async function getEmbeddingModelInfo({ env = process.env } = {}) {
96
110
  const modelId = resolveModelId(env);
97
- const loader = ADAPTERS[modelId] || ADAPTERS.hashing;
111
+ const loader = ADAPTERS[modelId];
98
112
  const { getModelInfo } = await loader();
99
113
  return getModelInfo({ env });
100
114
  }