@soleri/core 2.5.0 → 2.6.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.
- package/dist/brain/intelligence.d.ts +1 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +164 -148
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/control/identity-manager.d.ts +3 -1
- package/dist/control/identity-manager.d.ts.map +1 -1
- package/dist/control/identity-manager.js +49 -51
- package/dist/control/identity-manager.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +32 -32
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +1 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +48 -99
- package/dist/curator/curator.js.map +1 -1
- package/dist/governance/governance.d.ts +1 -0
- package/dist/governance/governance.d.ts.map +1 -1
- package/dist/governance/governance.js +51 -68
- package/dist/governance/governance.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/persistence/index.d.ts +2 -1
- package/dist/persistence/index.d.ts.map +1 -1
- package/dist/persistence/index.js +1 -0
- package/dist/persistence/index.js.map +1 -1
- package/dist/persistence/postgres-provider.d.ts +46 -0
- package/dist/persistence/postgres-provider.d.ts.map +1 -0
- package/dist/persistence/postgres-provider.js +115 -0
- package/dist/persistence/postgres-provider.js.map +1 -0
- package/dist/persistence/sqlite-provider.d.ts +5 -2
- package/dist/persistence/sqlite-provider.d.ts.map +1 -1
- package/dist/persistence/sqlite-provider.js +39 -1
- package/dist/persistence/sqlite-provider.js.map +1 -1
- package/dist/persistence/types.d.ts +23 -1
- package/dist/persistence/types.d.ts.map +1 -1
- package/dist/project/project-registry.d.ts +4 -4
- package/dist/project/project-registry.d.ts.map +1 -1
- package/dist/project/project-registry.js +25 -50
- package/dist/project/project-registry.js.map +1 -1
- package/dist/runtime/admin-extra-ops.d.ts +3 -3
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +29 -3
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/core-ops.d.ts +4 -4
- package/dist/runtime/core-ops.js +4 -4
- package/dist/runtime/runtime.js +1 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +3 -2
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +40 -2
- package/dist/runtime/vault-extra-ops.js.map +1 -1
- package/dist/vault/vault.d.ts +21 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +99 -0
- package/dist/vault/vault.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/admin-extra-ops.test.ts +2 -2
- package/src/__tests__/core-ops.test.ts +8 -2
- package/src/__tests__/persistence.test.ts +66 -0
- package/src/__tests__/postgres-provider.test.ts +58 -0
- package/src/__tests__/vault-extra-ops.test.ts +2 -2
- package/src/__tests__/vault.test.ts +184 -0
- package/src/brain/intelligence.ts +258 -307
- package/src/control/identity-manager.ts +77 -75
- package/src/control/intent-router.ts +55 -57
- package/src/curator/curator.ts +124 -145
- package/src/governance/governance.ts +90 -107
- package/src/index.ts +2 -0
- package/src/persistence/index.ts +2 -0
- package/src/persistence/postgres-provider.ts +157 -0
- package/src/persistence/sqlite-provider.ts +55 -2
- package/src/persistence/types.ts +31 -1
- package/src/project/project-registry.ts +69 -74
- package/src/runtime/admin-extra-ops.ts +36 -3
- package/src/runtime/core-ops.ts +4 -4
- package/src/runtime/runtime.ts +1 -1
- package/src/runtime/vault-extra-ops.ts +42 -2
- package/src/vault/vault.ts +118 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Vault } from '../vault/vault.js';
|
|
2
|
+
import type { PersistenceProvider } from '../persistence/types.js';
|
|
2
3
|
import type {
|
|
3
4
|
PolicyType,
|
|
4
5
|
PolicyPreset,
|
|
@@ -73,17 +74,18 @@ const DEFAULT_PRESET: PolicyPreset = 'moderate';
|
|
|
73
74
|
|
|
74
75
|
export class Governance {
|
|
75
76
|
private vault: Vault;
|
|
77
|
+
private provider: PersistenceProvider;
|
|
76
78
|
|
|
77
79
|
constructor(vault: Vault) {
|
|
78
80
|
this.vault = vault;
|
|
81
|
+
this.provider = vault.getProvider();
|
|
79
82
|
this.initializeTables();
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
// ─── Schema ─────────────────────────────────────────────────────
|
|
83
86
|
|
|
84
87
|
private initializeTables(): void {
|
|
85
|
-
|
|
86
|
-
db.exec(`
|
|
88
|
+
this.provider.execSql(`
|
|
87
89
|
CREATE TABLE IF NOT EXISTS vault_policies (
|
|
88
90
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
91
|
project_path TEXT NOT NULL,
|
|
@@ -147,14 +149,12 @@ export class Governance {
|
|
|
147
149
|
// ─── Policy CRUD ────────────────────────────────────────────────
|
|
148
150
|
|
|
149
151
|
getPolicy(projectPath: string): VaultPolicy {
|
|
150
|
-
const db = this.vault.getDb();
|
|
151
152
|
const defaults = POLICY_PRESETS[DEFAULT_PRESET];
|
|
152
153
|
|
|
153
|
-
const rows =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.all(projectPath) as Array<{ policy_type: string; config: string }>;
|
|
154
|
+
const rows = this.provider.all<{ policy_type: string; config: string }>(
|
|
155
|
+
'SELECT policy_type, config FROM vault_policies WHERE project_path = ? AND enabled = 1',
|
|
156
|
+
[projectPath],
|
|
157
|
+
);
|
|
158
158
|
|
|
159
159
|
let quotas = defaults.quotas;
|
|
160
160
|
let retention = defaults.retention;
|
|
@@ -176,26 +176,27 @@ export class Governance {
|
|
|
176
176
|
config: Record<string, unknown>,
|
|
177
177
|
changedBy?: string,
|
|
178
178
|
): void {
|
|
179
|
-
const db = this.vault.getDb();
|
|
180
|
-
|
|
181
179
|
// Get old config for audit trail
|
|
182
|
-
const existing =
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
const existing = this.provider.get<{ config: string }>(
|
|
181
|
+
'SELECT config FROM vault_policies WHERE project_path = ? AND policy_type = ?',
|
|
182
|
+
[projectPath, policyType],
|
|
183
|
+
);
|
|
185
184
|
const oldConfig = existing ? existing.config : null;
|
|
186
185
|
|
|
187
186
|
// UPSERT policy
|
|
188
|
-
|
|
187
|
+
this.provider.run(
|
|
189
188
|
`INSERT INTO vault_policies (project_path, policy_type, config, updated_at)
|
|
190
189
|
VALUES (?, ?, ?, unixepoch())
|
|
191
190
|
ON CONFLICT(project_path, policy_type)
|
|
192
191
|
DO UPDATE SET config = excluded.config, updated_at = excluded.updated_at`,
|
|
193
|
-
|
|
192
|
+
[projectPath, policyType, JSON.stringify(config)],
|
|
193
|
+
);
|
|
194
194
|
|
|
195
195
|
// Audit trail
|
|
196
|
-
|
|
196
|
+
this.provider.run(
|
|
197
197
|
'INSERT INTO vault_policy_changes (project_path, policy_type, old_config, new_config, changed_by) VALUES (?, ?, ?, ?, ?)',
|
|
198
|
-
|
|
198
|
+
[projectPath, policyType, oldConfig, JSON.stringify(config), changedBy ?? null],
|
|
199
|
+
);
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
applyPreset(projectPath: string, preset: PolicyPreset, changedBy?: string): void {
|
|
@@ -224,24 +225,23 @@ export class Governance {
|
|
|
224
225
|
|
|
225
226
|
getQuotaStatus(projectPath: string): QuotaStatus {
|
|
226
227
|
const policy = this.getPolicy(projectPath);
|
|
227
|
-
const db = this.vault.getDb();
|
|
228
228
|
|
|
229
|
-
const totalRow =
|
|
230
|
-
const total = totalRow
|
|
229
|
+
const totalRow = this.provider.get<{ count: number }>('SELECT COUNT(*) as count FROM entries');
|
|
230
|
+
const total = totalRow?.count ?? 0;
|
|
231
231
|
|
|
232
232
|
// Count by domain (used as category proxy)
|
|
233
|
-
const categoryRows =
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
const categoryRows = this.provider.all<{ domain: string; count: number }>(
|
|
234
|
+
'SELECT domain, COUNT(*) as count FROM entries GROUP BY domain',
|
|
235
|
+
);
|
|
236
236
|
const byCategory: Record<string, number> = {};
|
|
237
237
|
for (const row of categoryRows) {
|
|
238
238
|
byCategory[row.domain] = row.count;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// Count by type
|
|
242
|
-
const typeRows =
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
const typeRows = this.provider.all<{ type: string; count: number }>(
|
|
243
|
+
'SELECT type, COUNT(*) as count FROM entries GROUP BY type',
|
|
244
|
+
);
|
|
245
245
|
const byType: Record<string, number> = {};
|
|
246
246
|
for (const row of typeRows) {
|
|
247
247
|
byType[row.type] = row.count;
|
|
@@ -261,12 +261,7 @@ export class Governance {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
getAuditTrail(projectPath: string, limit?: number): PolicyAuditEntry[] {
|
|
264
|
-
const
|
|
265
|
-
const rows = db
|
|
266
|
-
.prepare(
|
|
267
|
-
'SELECT id, project_path, policy_type, old_config, new_config, changed_by, changed_at FROM vault_policy_changes WHERE project_path = ? ORDER BY changed_at DESC LIMIT ?',
|
|
268
|
-
)
|
|
269
|
-
.all(projectPath, limit ?? 50) as Array<{
|
|
264
|
+
const rows = this.provider.all<{
|
|
270
265
|
id: number;
|
|
271
266
|
project_path: string;
|
|
272
267
|
policy_type: string;
|
|
@@ -274,7 +269,10 @@ export class Governance {
|
|
|
274
269
|
new_config: string;
|
|
275
270
|
changed_by: string | null;
|
|
276
271
|
changed_at: number;
|
|
277
|
-
}
|
|
272
|
+
}>(
|
|
273
|
+
'SELECT id, project_path, policy_type, old_config, new_config, changed_by, changed_at FROM vault_policy_changes WHERE project_path = ? ORDER BY changed_at DESC LIMIT ?',
|
|
274
|
+
[projectPath, limit ?? 50],
|
|
275
|
+
);
|
|
278
276
|
|
|
279
277
|
return rows.map((row) => ({
|
|
280
278
|
id: row.id,
|
|
@@ -376,20 +374,20 @@ export class Governance {
|
|
|
376
374
|
decision: PolicyDecision,
|
|
377
375
|
): void {
|
|
378
376
|
try {
|
|
379
|
-
|
|
380
|
-
db.prepare(
|
|
377
|
+
this.provider.run(
|
|
381
378
|
`INSERT INTO vault_policy_evaluations
|
|
382
379
|
(project_path, entry_type, entry_category, entry_title, action, reason, quota_total, quota_max)
|
|
383
380
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
381
|
+
[
|
|
382
|
+
projectPath,
|
|
383
|
+
entry.type,
|
|
384
|
+
entry.category,
|
|
385
|
+
entry.title ?? null,
|
|
386
|
+
decision.action,
|
|
387
|
+
decision.reason ?? null,
|
|
388
|
+
decision.quotaStatus?.total ?? null,
|
|
389
|
+
decision.quotaStatus?.maxTotal ?? null,
|
|
390
|
+
],
|
|
393
391
|
);
|
|
394
392
|
} catch {
|
|
395
393
|
// Fire-and-forget — don't fail capture because of evaluation logging
|
|
@@ -409,13 +407,10 @@ export class Governance {
|
|
|
409
407
|
},
|
|
410
408
|
source?: string,
|
|
411
409
|
): number {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
417
|
-
)
|
|
418
|
-
.run(
|
|
410
|
+
const result = this.provider.run(
|
|
411
|
+
`INSERT INTO vault_proposals (project_path, entry_id, title, type, category, proposed_data, source)
|
|
412
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
413
|
+
[
|
|
419
414
|
projectPath,
|
|
420
415
|
entryData.entryId ?? null,
|
|
421
416
|
entryData.title,
|
|
@@ -423,7 +418,8 @@ export class Governance {
|
|
|
423
418
|
entryData.category,
|
|
424
419
|
JSON.stringify(entryData.data ?? {}),
|
|
425
420
|
source ?? 'auto-capture',
|
|
426
|
-
|
|
421
|
+
],
|
|
422
|
+
);
|
|
427
423
|
return Number(result.lastInsertRowid);
|
|
428
424
|
}
|
|
429
425
|
|
|
@@ -443,23 +439,23 @@ export class Governance {
|
|
|
443
439
|
modifications: Record<string, unknown>,
|
|
444
440
|
decidedBy?: string,
|
|
445
441
|
): Proposal | null {
|
|
446
|
-
const db = this.vault.getDb();
|
|
447
442
|
const existing = this.getProposalById(proposalId);
|
|
448
443
|
if (!existing || existing.status !== 'pending') return null;
|
|
449
444
|
|
|
450
445
|
// Merge modifications into proposed data
|
|
451
446
|
const merged = { ...existing.proposedData, ...modifications };
|
|
452
447
|
|
|
453
|
-
|
|
448
|
+
this.provider.run(
|
|
454
449
|
`UPDATE vault_proposals
|
|
455
450
|
SET status = 'modified', proposed_data = ?, decided_at = unixepoch(),
|
|
456
451
|
decided_by = ?, modification_note = ?
|
|
457
452
|
WHERE id = ?`,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
453
|
+
[
|
|
454
|
+
JSON.stringify(merged),
|
|
455
|
+
decidedBy ?? null,
|
|
456
|
+
`Modified fields: ${Object.keys(modifications).join(', ')}`,
|
|
457
|
+
proposalId,
|
|
458
|
+
],
|
|
463
459
|
);
|
|
464
460
|
|
|
465
461
|
const proposal = this.getProposalById(proposalId);
|
|
@@ -468,31 +464,28 @@ export class Governance {
|
|
|
468
464
|
}
|
|
469
465
|
|
|
470
466
|
listPendingProposals(projectPath?: string, limit?: number): Proposal[] {
|
|
471
|
-
const db = this.vault.getDb();
|
|
472
467
|
if (projectPath) {
|
|
473
|
-
const rows =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return (rows as RawProposal[]).map(mapProposal);
|
|
468
|
+
const rows = this.provider.all<RawProposal>(
|
|
469
|
+
'SELECT * FROM vault_proposals WHERE project_path = ? AND status = ? ORDER BY proposed_at DESC LIMIT ?',
|
|
470
|
+
[projectPath, 'pending', limit ?? 50],
|
|
471
|
+
);
|
|
472
|
+
return rows.map(mapProposal);
|
|
479
473
|
}
|
|
480
|
-
const rows =
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
474
|
+
const rows = this.provider.all<RawProposal>(
|
|
475
|
+
'SELECT * FROM vault_proposals WHERE status = ? ORDER BY proposed_at DESC LIMIT ?',
|
|
476
|
+
['pending', limit ?? 50],
|
|
477
|
+
);
|
|
478
|
+
return rows.map(mapProposal);
|
|
484
479
|
}
|
|
485
480
|
|
|
486
481
|
getProposalStats(projectPath?: string): ProposalStats {
|
|
487
|
-
const db = this.vault.getDb();
|
|
488
482
|
const whereClause = projectPath ? 'WHERE project_path = ?' : '';
|
|
489
483
|
const params = projectPath ? [projectPath] : [];
|
|
490
484
|
|
|
491
|
-
const statusRows =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
.all(...params) as Array<{ status: string; count: number }>;
|
|
485
|
+
const statusRows = this.provider.all<{ status: string; count: number }>(
|
|
486
|
+
`SELECT status, COUNT(*) as count FROM vault_proposals ${whereClause} GROUP BY status`,
|
|
487
|
+
params,
|
|
488
|
+
);
|
|
496
489
|
|
|
497
490
|
const stats: ProposalStats = {
|
|
498
491
|
total: 0,
|
|
@@ -530,11 +523,10 @@ export class Governance {
|
|
|
530
523
|
stats.acceptanceRate = decided > 0 ? (stats.approved + stats.modified) / decided : 0;
|
|
531
524
|
|
|
532
525
|
// Category breakdown
|
|
533
|
-
const catRows =
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
.all(...params) as Array<{ category: string; status: string; count: number }>;
|
|
526
|
+
const catRows = this.provider.all<{ category: string; status: string; count: number }>(
|
|
527
|
+
`SELECT category, status, COUNT(*) as count FROM vault_proposals ${whereClause} GROUP BY category, status`,
|
|
528
|
+
params,
|
|
529
|
+
);
|
|
538
530
|
|
|
539
531
|
for (const row of catRows) {
|
|
540
532
|
if (!stats.byCategory[row.category]) {
|
|
@@ -554,15 +546,13 @@ export class Governance {
|
|
|
554
546
|
}
|
|
555
547
|
|
|
556
548
|
expireStaleProposals(maxAgeDays?: number): number {
|
|
557
|
-
const db = this.vault.getDb();
|
|
558
549
|
const days = maxAgeDays ?? 14;
|
|
559
550
|
const cutoff = Math.floor(Date.now() / 1000) - days * 86400;
|
|
560
551
|
|
|
561
|
-
const result =
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
.run(cutoff);
|
|
552
|
+
const result = this.provider.run(
|
|
553
|
+
"UPDATE vault_proposals SET status = 'expired', decided_at = unixepoch() WHERE status = 'pending' AND proposed_at < ?",
|
|
554
|
+
[cutoff],
|
|
555
|
+
);
|
|
566
556
|
|
|
567
557
|
return result.changes;
|
|
568
558
|
}
|
|
@@ -575,13 +565,11 @@ export class Governance {
|
|
|
575
565
|
const proposalStats = this.getProposalStats(projectPath);
|
|
576
566
|
|
|
577
567
|
// Evaluation trend — count by action in last 7 days
|
|
578
|
-
const db = this.vault.getDb();
|
|
579
568
|
const sevenDaysAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
580
|
-
const trendRows =
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
.all(projectPath, sevenDaysAgo) as Array<{ action: string; count: number }>;
|
|
569
|
+
const trendRows = this.provider.all<{ action: string; count: number }>(
|
|
570
|
+
'SELECT action, COUNT(*) as count FROM vault_policy_evaluations WHERE project_path = ? AND evaluated_at > ? GROUP BY action',
|
|
571
|
+
[projectPath, sevenDaysAgo],
|
|
572
|
+
);
|
|
585
573
|
|
|
586
574
|
const evaluationTrend: Record<string, number> = {};
|
|
587
575
|
for (const row of trendRows) {
|
|
@@ -608,13 +596,11 @@ export class Governance {
|
|
|
608
596
|
// ─── Private Helpers ────────────────────────────────────────────
|
|
609
597
|
|
|
610
598
|
private countPending(projectPath: string): number {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
.get(projectPath) as { count: number };
|
|
617
|
-
return row.count;
|
|
599
|
+
const row = this.provider.get<{ count: number }>(
|
|
600
|
+
"SELECT COUNT(*) as count FROM vault_proposals WHERE project_path = ? AND status = 'pending'",
|
|
601
|
+
[projectPath],
|
|
602
|
+
);
|
|
603
|
+
return row?.count ?? 0;
|
|
618
604
|
}
|
|
619
605
|
|
|
620
606
|
private captureFromProposal(proposal: Proposal): void {
|
|
@@ -641,22 +627,19 @@ export class Governance {
|
|
|
641
627
|
decidedBy?: string,
|
|
642
628
|
note?: string,
|
|
643
629
|
): Proposal | null {
|
|
644
|
-
const db = this.vault.getDb();
|
|
645
630
|
const existing = this.getProposalById(proposalId);
|
|
646
631
|
if (!existing || existing.status !== 'pending') return null;
|
|
647
632
|
|
|
648
|
-
|
|
633
|
+
this.provider.run(
|
|
649
634
|
'UPDATE vault_proposals SET status = ?, decided_at = unixepoch(), decided_by = ?, modification_note = ? WHERE id = ?',
|
|
650
|
-
|
|
635
|
+
[status, decidedBy ?? null, note ?? null, proposalId],
|
|
636
|
+
);
|
|
651
637
|
|
|
652
638
|
return this.getProposalById(proposalId);
|
|
653
639
|
}
|
|
654
640
|
|
|
655
641
|
private getProposalById(id: number): Proposal | null {
|
|
656
|
-
const
|
|
657
|
-
const row = db.prepare('SELECT * FROM vault_proposals WHERE id = ?').get(id) as
|
|
658
|
-
| RawProposal
|
|
659
|
-
| undefined;
|
|
642
|
+
const row = this.provider.get<RawProposal>('SELECT * FROM vault_proposals WHERE id = ?', [id]);
|
|
660
643
|
return row ? mapProposal(row) : null;
|
|
661
644
|
}
|
|
662
645
|
}
|
package/src/index.ts
CHANGED
|
@@ -328,11 +328,13 @@ export type {
|
|
|
328
328
|
|
|
329
329
|
// ─── Persistence ───────────────────────────────────────────────────────
|
|
330
330
|
export { SQLitePersistenceProvider } from './persistence/index.js';
|
|
331
|
+
export { PostgresPersistenceProvider, translateSql } from './persistence/index.js';
|
|
331
332
|
export type {
|
|
332
333
|
PersistenceProvider,
|
|
333
334
|
PersistenceParams,
|
|
334
335
|
RunResult,
|
|
335
336
|
PersistenceConfig,
|
|
337
|
+
FtsSearchOptions,
|
|
336
338
|
} from './persistence/index.js';
|
|
337
339
|
|
|
338
340
|
// ─── Prompts ───────────────────────────────────────────────────────────
|
package/src/persistence/index.ts
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL persistence provider (working stub).
|
|
3
|
+
*
|
|
4
|
+
* Implements PersistenceProvider with pg.Pool. The translateSql() function
|
|
5
|
+
* converts SQLite-style queries to PostgreSQL-compatible syntax.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: PersistenceProvider is synchronous (better-sqlite3 heritage).
|
|
8
|
+
* This provider wraps async pg calls synchronously for interface compliance.
|
|
9
|
+
* Full async provider support is planned for v7.0.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
PersistenceProvider,
|
|
14
|
+
PersistenceParams,
|
|
15
|
+
RunResult,
|
|
16
|
+
FtsSearchOptions,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Translate SQLite-style SQL to PostgreSQL-compatible SQL.
|
|
21
|
+
*
|
|
22
|
+
* - Converts positional `?` params to `$1, $2, ...`
|
|
23
|
+
* - Converts named `@name` params to `$N` positional, returns ordered values
|
|
24
|
+
* - Replaces `unixepoch()` with `EXTRACT(EPOCH FROM NOW())::integer`
|
|
25
|
+
*/
|
|
26
|
+
export function translateSql(
|
|
27
|
+
sql: string,
|
|
28
|
+
params?: PersistenceParams,
|
|
29
|
+
): { sql: string; values: unknown[] } {
|
|
30
|
+
let translated = sql.replace(/unixepoch\(\)/gi, 'EXTRACT(EPOCH FROM NOW())::integer');
|
|
31
|
+
|
|
32
|
+
if (!params) return { sql: translated, values: [] };
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(params)) {
|
|
35
|
+
let idx = 0;
|
|
36
|
+
translated = translated.replace(/\?/g, () => `$${++idx}`);
|
|
37
|
+
return { sql: translated, values: params };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Named params: @name -> $N
|
|
41
|
+
const values: unknown[] = [];
|
|
42
|
+
const nameMap = new Map<string, number>();
|
|
43
|
+
translated = translated.replace(/@(\w+)/g, (_match, name: string) => {
|
|
44
|
+
if (!nameMap.has(name)) {
|
|
45
|
+
nameMap.set(name, values.length + 1);
|
|
46
|
+
values.push(params[name]);
|
|
47
|
+
}
|
|
48
|
+
return `$${nameMap.get(name)}`;
|
|
49
|
+
});
|
|
50
|
+
return { sql: translated, values };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* PostgreSQL persistence provider.
|
|
55
|
+
*
|
|
56
|
+
* Uses pg.Pool for connection management. Created via async factory
|
|
57
|
+
* `PostgresPersistenceProvider.create()`.
|
|
58
|
+
*/
|
|
59
|
+
export class PostgresPersistenceProvider implements PersistenceProvider {
|
|
60
|
+
readonly backend = 'postgres' as const;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
private pool: any;
|
|
63
|
+
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
private constructor(pool: any) {
|
|
66
|
+
this.pool = pool;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Async factory. Dynamically imports `pg` (optional dependency).
|
|
71
|
+
*/
|
|
72
|
+
static async create(
|
|
73
|
+
connectionString: string,
|
|
74
|
+
poolSize = 10,
|
|
75
|
+
): Promise<PostgresPersistenceProvider> {
|
|
76
|
+
const { default: pg } = await import('pg');
|
|
77
|
+
const pool = new pg.Pool({
|
|
78
|
+
connectionString,
|
|
79
|
+
max: poolSize,
|
|
80
|
+
idleTimeoutMillis: 30_000,
|
|
81
|
+
});
|
|
82
|
+
return new PostgresPersistenceProvider(pool);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
execSql(sql: string): void {
|
|
86
|
+
// Sync wrapper -- logs warning in non-test env
|
|
87
|
+
// Full async support in v7.0
|
|
88
|
+
void sql;
|
|
89
|
+
throw new Error(
|
|
90
|
+
'PostgresPersistenceProvider.execSql() is not yet implemented. ' +
|
|
91
|
+
'Use SQLitePersistenceProvider for synchronous operations. ' +
|
|
92
|
+
'Full PostgreSQL support requires async PersistenceProvider (v7.0).',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
run(sql: string, params?: PersistenceParams): RunResult {
|
|
97
|
+
const _translated = translateSql(sql, params);
|
|
98
|
+
throw new Error(
|
|
99
|
+
'PostgresPersistenceProvider.run() is not yet implemented. ' +
|
|
100
|
+
'Full PostgreSQL support requires async PersistenceProvider (v7.0).',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get<T = Record<string, unknown>>(sql: string, params?: PersistenceParams): T | undefined {
|
|
105
|
+
const _translated = translateSql(sql, params);
|
|
106
|
+
throw new Error(
|
|
107
|
+
'PostgresPersistenceProvider.get() is not yet implemented. ' +
|
|
108
|
+
'Full PostgreSQL support requires async PersistenceProvider (v7.0).',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
all<T = Record<string, unknown>>(sql: string, params?: PersistenceParams): T[] {
|
|
113
|
+
const _translated = translateSql(sql, params);
|
|
114
|
+
throw new Error(
|
|
115
|
+
'PostgresPersistenceProvider.all() is not yet implemented. ' +
|
|
116
|
+
'Full PostgreSQL support requires async PersistenceProvider (v7.0).',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
transaction<T>(fn: () => T): T {
|
|
121
|
+
void fn;
|
|
122
|
+
throw new Error(
|
|
123
|
+
'PostgresPersistenceProvider.transaction() is not yet implemented. ' +
|
|
124
|
+
'Full PostgreSQL support requires async PersistenceProvider (v7.0).',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ftsSearch<T = Record<string, unknown>>(
|
|
129
|
+
table: string,
|
|
130
|
+
query: string,
|
|
131
|
+
options?: FtsSearchOptions,
|
|
132
|
+
): T[] {
|
|
133
|
+
const _cols = options?.columns?.length ? options.columns.join(', ') : '*';
|
|
134
|
+
const _limit = options?.limit ?? 50;
|
|
135
|
+
const _offset = options?.offset ?? 0;
|
|
136
|
+
void table;
|
|
137
|
+
void query;
|
|
138
|
+
// Would generate: SELECT cols FROM table WHERE tsvector_col @@ to_tsquery($1)
|
|
139
|
+
// ORDER BY ts_rank(tsvector_col, to_tsquery($1)) DESC LIMIT $2 OFFSET $3
|
|
140
|
+
throw new Error(
|
|
141
|
+
'PostgresPersistenceProvider.ftsSearch() is not yet implemented. ' +
|
|
142
|
+
'Full PostgreSQL support requires async PersistenceProvider (v7.0).',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ftsRebuild(table: string): void {
|
|
147
|
+
// Would run: REINDEX INDEX idx_{table}_fts
|
|
148
|
+
void table;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
close(): void {
|
|
152
|
+
if (this.pool) {
|
|
153
|
+
// Fire-and-forget pool end; sync interface constraint
|
|
154
|
+
void (this.pool.end() as Promise<void>);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -8,14 +8,25 @@
|
|
|
8
8
|
import Database from 'better-sqlite3';
|
|
9
9
|
import { mkdirSync } from 'node:fs';
|
|
10
10
|
import { dirname } from 'node:path';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
PersistenceProvider,
|
|
13
|
+
PersistenceParams,
|
|
14
|
+
RunResult,
|
|
15
|
+
FtsSearchOptions,
|
|
16
|
+
} from './types.js';
|
|
12
17
|
|
|
13
18
|
export class SQLitePersistenceProvider implements PersistenceProvider {
|
|
19
|
+
readonly backend = 'sqlite' as const;
|
|
14
20
|
private db: Database.Database;
|
|
15
21
|
|
|
16
22
|
constructor(path: string = ':memory:') {
|
|
17
23
|
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
|
18
24
|
this.db = new Database(path);
|
|
25
|
+
if (path !== ':memory:') {
|
|
26
|
+
this.db.pragma('cache_size = -64000'); // 64MB
|
|
27
|
+
this.db.pragma('temp_store = MEMORY');
|
|
28
|
+
this.db.pragma('mmap_size = 268435456'); // 256MB
|
|
29
|
+
}
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
execSql(sql: string): void {
|
|
@@ -47,6 +58,43 @@ export class SQLitePersistenceProvider implements PersistenceProvider {
|
|
|
47
58
|
return this.db.transaction(fn)();
|
|
48
59
|
}
|
|
49
60
|
|
|
61
|
+
ftsSearch<T = Record<string, unknown>>(
|
|
62
|
+
table: string,
|
|
63
|
+
query: string,
|
|
64
|
+
options?: FtsSearchOptions,
|
|
65
|
+
): T[] {
|
|
66
|
+
const ftsTable = `${table}_fts`;
|
|
67
|
+
const cols = options?.columns?.length ? options.columns.join(', ') : `${table}.*`;
|
|
68
|
+
const orderBy = options?.orderByRank !== false ? `ORDER BY rank` : '';
|
|
69
|
+
const limit = options?.limit ? `LIMIT ${options.limit}` : '';
|
|
70
|
+
const offset = options?.offset ? `OFFSET ${options.offset}` : '';
|
|
71
|
+
|
|
72
|
+
const filterClauses: string[] = [];
|
|
73
|
+
const filterParams: unknown[] = [query];
|
|
74
|
+
|
|
75
|
+
if (options?.filters) {
|
|
76
|
+
for (const [key, value] of Object.entries(options.filters)) {
|
|
77
|
+
filterClauses.push(`${table}.${key} = ?`);
|
|
78
|
+
filterParams.push(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const filterSql = filterClauses.length ? `AND ${filterClauses.join(' AND ')}` : '';
|
|
83
|
+
|
|
84
|
+
const sql = `SELECT ${cols} FROM ${ftsTable} JOIN ${table} ON ${table}.rowid = ${ftsTable}.rowid WHERE ${ftsTable} MATCH ? ${filterSql} ${orderBy} ${limit} ${offset}`;
|
|
85
|
+
|
|
86
|
+
return this.all<T>(sql, filterParams);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ftsRebuild(table: string): void {
|
|
90
|
+
const ftsTable = `${table}_fts`;
|
|
91
|
+
try {
|
|
92
|
+
this.execSql(`INSERT INTO ${ftsTable}(${ftsTable}) VALUES('rebuild')`);
|
|
93
|
+
} catch {
|
|
94
|
+
// Graceful degradation: FTS table may not exist
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
50
98
|
close(): void {
|
|
51
99
|
this.db.close();
|
|
52
100
|
}
|
|
@@ -54,9 +102,14 @@ export class SQLitePersistenceProvider implements PersistenceProvider {
|
|
|
54
102
|
/**
|
|
55
103
|
* Escape hatch: get the raw better-sqlite3 Database.
|
|
56
104
|
* Used by modules that need direct db access (ProjectRegistry, BrainIntelligence, etc.).
|
|
57
|
-
*
|
|
105
|
+
* @deprecated Use provider methods instead.
|
|
58
106
|
*/
|
|
59
107
|
getDatabase(): Database.Database {
|
|
108
|
+
if (process.env.NODE_ENV !== 'test' && process.env.VITEST !== 'true') {
|
|
109
|
+
console.warn(
|
|
110
|
+
'SQLitePersistenceProvider.getDatabase() is deprecated. Use provider methods instead.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
60
113
|
return this.db;
|
|
61
114
|
}
|
|
62
115
|
}
|
package/src/persistence/types.ts
CHANGED
|
@@ -34,11 +34,41 @@ export interface PersistenceProvider {
|
|
|
34
34
|
/** Run a function inside a transaction. Commits on success, rolls back on error. */
|
|
35
35
|
transaction<T>(fn: () => T): T;
|
|
36
36
|
|
|
37
|
+
/** Identifies the backend engine. */
|
|
38
|
+
readonly backend: 'sqlite' | 'postgres';
|
|
39
|
+
|
|
40
|
+
/** Full-text search abstraction. */
|
|
41
|
+
ftsSearch<T = Record<string, unknown>>(
|
|
42
|
+
table: string,
|
|
43
|
+
query: string,
|
|
44
|
+
options?: FtsSearchOptions,
|
|
45
|
+
): T[];
|
|
46
|
+
|
|
47
|
+
/** Rebuild FTS index for a table. */
|
|
48
|
+
ftsRebuild(table: string): void;
|
|
49
|
+
|
|
37
50
|
/** Close the connection. */
|
|
38
51
|
close(): void;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
export interface PersistenceConfig {
|
|
42
|
-
type: 'sqlite';
|
|
55
|
+
type: 'sqlite' | 'postgres';
|
|
43
56
|
path: string;
|
|
57
|
+
/** PostgreSQL connection string. */
|
|
58
|
+
connectionString?: string;
|
|
59
|
+
/** PostgreSQL pool size. */
|
|
60
|
+
poolSize?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface FtsSearchOptions {
|
|
64
|
+
/** Columns to search (default: all FTS columns). */
|
|
65
|
+
columns?: string[];
|
|
66
|
+
/** Max results. */
|
|
67
|
+
limit?: number;
|
|
68
|
+
/** Skip N results. */
|
|
69
|
+
offset?: number;
|
|
70
|
+
/** Additional WHERE conditions on the base table. */
|
|
71
|
+
filters?: Record<string, unknown>;
|
|
72
|
+
/** Order by FTS relevance rank (default: true). */
|
|
73
|
+
orderByRank?: boolean;
|
|
44
74
|
}
|