@geraldmaron/construct 1.0.21 → 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.
Files changed (150) hide show
  1. package/README.md +23 -7
  2. package/bin/construct +206 -228
  3. package/bin/construct-postinstall.mjs +25 -15
  4. package/lib/acp/server.mjs +113 -0
  5. package/lib/agent-instructions/inject.mjs +94 -0
  6. package/lib/auto-docs.mjs +10 -2
  7. package/lib/cli-commands.mjs +43 -15
  8. package/lib/comment-lint.mjs +115 -0
  9. package/lib/completions.mjs +7 -1
  10. package/lib/config/schema.mjs +4 -0
  11. package/lib/decisions/enforced-baseline.json +2 -0
  12. package/lib/docs-verify.mjs +15 -18
  13. package/lib/doctor/cli.mjs +8 -1
  14. package/lib/document-export.mjs +124 -0
  15. package/lib/embed/daemon.mjs +1 -1
  16. package/lib/embed/inbox.mjs +31 -10
  17. package/lib/embed/recommendation-store.mjs +7 -289
  18. package/lib/embed/reconcile.mjs +2 -2
  19. package/lib/features.mjs +11 -11
  20. package/lib/git-hooks-path.mjs +61 -0
  21. package/lib/home-namespace.mjs +60 -0
  22. package/lib/hooks/ci-status-check.mjs +62 -40
  23. package/lib/hooks/config-protection.mjs +4 -4
  24. package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
  25. package/lib/hooks/pre-push-gate.mjs +15 -6
  26. package/lib/hooks/session-reflect.mjs +5 -1
  27. package/lib/hooks/stop-notify.mjs +32 -17
  28. package/lib/hooks/stop-typecheck.mjs +7 -2
  29. package/lib/host-capabilities.mjs +24 -8
  30. package/lib/host-disposition.mjs +76 -0
  31. package/lib/ingest/provider-extract.mjs +1 -1
  32. package/lib/init-docs.mjs +1 -1
  33. package/lib/init-unified.mjs +320 -219
  34. package/lib/init-update.mjs +4 -84
  35. package/lib/init.mjs +9 -25
  36. package/lib/install/stage-project.mjs +8 -2
  37. package/lib/intake/git-queue.mjs +195 -0
  38. package/lib/intake/queue.mjs +9 -16
  39. package/lib/intent-classifier.mjs +1 -1
  40. package/lib/knowledge/search.mjs +52 -3
  41. package/lib/mcp/server.mjs +57 -14
  42. package/lib/mcp/tools/memory.mjs +2 -2
  43. package/lib/mcp/tools/orchestration-run.mjs +125 -0
  44. package/lib/mcp/tools/storage.mjs +2 -3
  45. package/lib/mcp-catalog.json +3 -3
  46. package/lib/mcp-manager.mjs +59 -3
  47. package/lib/model-registry.mjs +40 -33
  48. package/lib/observation-store.mjs +39 -163
  49. package/lib/opencode-config.mjs +1 -1
  50. package/lib/orchestration/events.mjs +66 -0
  51. package/lib/orchestration/runtime.mjs +72 -10
  52. package/lib/orchestration/worker.mjs +69 -21
  53. package/lib/orchestration-policy.mjs +27 -3
  54. package/lib/parity.mjs +46 -24
  55. package/lib/policy/unified-gates.mjs +96 -0
  56. package/lib/project-init-shared.mjs +0 -173
  57. package/lib/reconcile/adapter-prune.mjs +105 -0
  58. package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
  59. package/lib/reconcile/gitignore-coverage.mjs +88 -0
  60. package/lib/reconcile/index.mjs +169 -0
  61. package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
  62. package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
  63. package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
  64. package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
  65. package/lib/runtime/uv-bootstrap.mjs +27 -3
  66. package/lib/schema-infer.mjs +16 -2
  67. package/lib/server/csrf.mjs +14 -2
  68. package/lib/server/index.mjs +95 -0
  69. package/lib/service-manager.mjs +59 -252
  70. package/lib/setup-prompts.mjs +2 -1
  71. package/lib/setup.mjs +104 -480
  72. package/lib/status.mjs +3 -6
  73. package/lib/storage/admin.mjs +48 -325
  74. package/lib/storage/backend.mjs +10 -43
  75. package/lib/storage/hybrid-query.mjs +15 -196
  76. package/lib/storage/sync.mjs +36 -177
  77. package/lib/storage/vector-client.mjs +256 -235
  78. package/lib/strategy-store.mjs +35 -286
  79. package/lib/template-registry.mjs +73 -0
  80. package/lib/term-format.mjs +75 -0
  81. package/lib/uninstall/uninstall.mjs +180 -7
  82. package/lib/worker/entrypoint.mjs +6 -14
  83. package/package.json +7 -6
  84. package/personas/construct.md +7 -8
  85. package/platforms/claude/settings.template.json +30 -11
  86. package/platforms/opencode/config.template.json +2 -2
  87. package/rules/common/neurodivergent-output.md +66 -0
  88. package/rules/common/tool-invisibility.md +37 -0
  89. package/scripts/sync-specialists.mjs +427 -107
  90. package/skills/operating/orchestration-reference.md +2 -16
  91. package/specialists/policy-inventory.json +14 -0
  92. package/specialists/prompts/cx-accessibility.md +2 -6
  93. package/specialists/prompts/cx-ai-engineer.md +0 -4
  94. package/specialists/prompts/cx-architect.md +3 -5
  95. package/specialists/prompts/cx-business-strategist.md +0 -5
  96. package/specialists/prompts/cx-data-analyst.md +0 -4
  97. package/specialists/prompts/cx-data-engineer.md +0 -4
  98. package/specialists/prompts/cx-debugger.md +2 -6
  99. package/specialists/prompts/cx-designer.md +0 -8
  100. package/specialists/prompts/cx-devil-advocate.md +2 -2
  101. package/specialists/prompts/cx-docs-keeper.md +0 -13
  102. package/specialists/prompts/cx-engineer.md +0 -13
  103. package/specialists/prompts/cx-evaluator.md +2 -2
  104. package/specialists/prompts/cx-explorer.md +4 -5
  105. package/specialists/prompts/cx-legal-compliance.md +4 -5
  106. package/specialists/prompts/cx-operations.md +0 -5
  107. package/specialists/prompts/cx-orchestrator.md +0 -4
  108. package/specialists/prompts/cx-platform-engineer.md +0 -8
  109. package/specialists/prompts/cx-product-manager.md +0 -8
  110. package/specialists/prompts/cx-qa.md +3 -12
  111. package/specialists/prompts/cx-rd-lead.md +0 -5
  112. package/specialists/prompts/cx-release-manager.md +0 -8
  113. package/specialists/prompts/cx-researcher.md +5 -29
  114. package/specialists/prompts/cx-reviewer.md +2 -6
  115. package/specialists/prompts/cx-security.md +2 -11
  116. package/specialists/prompts/cx-sre.md +0 -15
  117. package/specialists/prompts/cx-test-automation.md +0 -4
  118. package/specialists/prompts/cx-trace-reviewer.md +2 -2
  119. package/specialists/prompts/cx-ux-researcher.md +0 -4
  120. package/specialists/registry.json +28 -36
  121. package/templates/distribution/run.mjs +36 -7
  122. package/templates/docs/accessibility-audit.md +56 -0
  123. package/templates/docs/architecture-review.md +59 -0
  124. package/templates/docs/code-review-report.md +46 -0
  125. package/templates/docs/construct_guide.md +15 -15
  126. package/templates/docs/debug-investigation.md +53 -0
  127. package/templates/docs/qa-report.md +48 -0
  128. package/templates/docs/security-audit-report.md +48 -0
  129. package/templates/docs/task-packet.md +49 -0
  130. package/templates/docs/verdict.md +40 -0
  131. package/db/schema/001_init.sql +0 -40
  132. package/db/schema/002_pgvector.sql +0 -182
  133. package/db/schema/003_intake.sql +0 -47
  134. package/db/schema/003_observation_reconciliation.sql +0 -14
  135. package/db/schema/004_recommendations.sql +0 -46
  136. package/db/schema/005_strategy.sql +0 -21
  137. package/db/schema/006_graph.sql +0 -24
  138. package/db/schema/007_tags.sql +0 -30
  139. package/db/schema/008_skill_usage.sql +0 -24
  140. package/db/schema/009_scheduler.sql +0 -14
  141. package/db/schema/010_cx_scores.sql +0 -51
  142. package/lib/intake/postgres-queue.mjs +0 -240
  143. package/lib/server/static/index.html +0 -1
  144. package/lib/services/local-postgres.mjs +0 -15
  145. package/lib/storage/backup.mjs +0 -347
  146. package/lib/storage/migrations.mjs +0 -187
  147. package/lib/storage/postgres-backup.mjs +0 -124
  148. package/lib/storage/sql-store.mjs +0 -45
  149. package/lib/storage/store-version.mjs +0 -115
  150. package/lib/storage/vector-store.mjs +0 -100
package/lib/status.mjs CHANGED
@@ -20,9 +20,6 @@ import { resolveExecutionContractModelMetadata, selectModelTierForWorkCategory }
20
20
  import { loadPluginRegistry } from './plugin-registry.mjs';
21
21
  import { readCostLog, summarizeCostData, normalizeCostEntry } from './cost.mjs';
22
22
  import { readEfficiencyLog, summarizeEfficiencyData } from './efficiency.mjs';
23
- import { describeSqlStore, sqlStoreHealth } from './storage/sql-store.mjs';
24
- import { describeVectorStore } from './storage/vector-store.mjs';
25
- import { describeSqlStoreHealth } from './storage/sql-store.mjs';
26
23
  import { triggerAutoBackfillIfSparse } from './telemetry/backfill.mjs';
27
24
  import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
28
25
  const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
@@ -709,9 +706,9 @@ export async function buildStatus({
709
706
  triggerAutoBackfillIfSparse(telemetryRichness, { ...mergedEnv });
710
707
  const efficiencyDigest = summarizeEfficiencyData(readEfficiencyLog(homeDir));
711
708
  const activeOverlays = getActiveOverlays(cwd);
712
- const sqlStore = describeSqlStore(mergedEnv);
713
- const vectorStore = describeVectorStore(mergedEnv);
714
- const sqlHealth = sqlStore.mode === 'postgres' ? await describeSqlStoreHealth(mergedEnv) : sqlStoreHealth(mergedEnv);
709
+ const sqlStore = { mode: 'lancedb', label: 'LanceDB + Git-Backed', dbUrl: null, vectorEnabled: true };
710
+ const vectorStore = { enabled: true, backend: 'lancedb', label: 'Embedded LanceDB' };
711
+ const sqlHealth = { status: 'healthy', message: 'Local embedded' };
715
712
  const promotionRequests = getPromotionRequests(cwd);
716
713
  const executionContractModel = resolveExecutionContractModelMetadata({
717
714
  envValues: mergedEnv,
@@ -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,55 +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
9
 
22
10
  export function createSqlClient(env = process.env) {
23
- const databaseUrl = cleanUrl(resolveDatabaseUrl(env));
24
- if (!databaseUrl || !postgres) return null;
25
- return postgres(databaseUrl, {
26
- max: Number.parseInt(env.CONSTRUCT_DB_POOL_SIZE || '5', 10),
27
- idle_timeout: Number.parseInt(env.CONSTRUCT_DB_IDLE_TIMEOUT_MS || '30000', 10),
28
- connect_timeout: Number.parseInt(env.CONSTRUCT_DB_CONNECT_TIMEOUT_MS || '5000', 10),
29
- });
11
+ // Construct no longer manages local Postgres. External DATABASE_URL is
12
+ // ignored for local-first operations.
13
+ return null;
30
14
  }
31
15
 
32
16
  export async function probeSqlClient(client) {
33
- if (!client) return { status: 'unavailable', message: 'No SQL client configured' };
34
- try {
35
- await client`select 1 as ok`;
36
- return { status: 'healthy', message: 'Shared Postgres store reachable' };
37
- } catch (error) {
38
- return { status: 'degraded', message: `SQL store unreachable: ${error?.message || 'unknown error'}` };
39
- }
17
+ return { status: 'unavailable', message: 'SQL backend is no longer used for local operations' };
40
18
  }
41
19
 
42
20
  export async function closeSqlClient(client) {
43
- if (!client) return;
44
- await client.end({ timeout: 5 });
45
- }
46
-
47
- export function readVectorConfig(env = process.env) {
48
- const endpoint = cleanUrl(env.CONSTRUCT_VECTOR_URL);
49
- const indexPath = cleanUrl(env.CONSTRUCT_VECTOR_INDEX_PATH);
50
- return {
51
- endpoint: endpoint || null,
52
- indexPath: indexPath || null,
53
- model: cleanUrl(env.CONSTRUCT_VECTOR_MODEL) || null,
54
- };
21
+ return Promise.resolve();
55
22
  }