chainlesschain 0.47.9 → 0.51.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 (73) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/codegen.js +303 -0
  10. package/src/commands/collab.js +482 -0
  11. package/src/commands/crosschain.js +382 -0
  12. package/src/commands/dbevo.js +388 -0
  13. package/src/commands/dev.js +411 -0
  14. package/src/commands/federation.js +427 -0
  15. package/src/commands/fusion.js +332 -0
  16. package/src/commands/governance.js +505 -0
  17. package/src/commands/hardening.js +110 -0
  18. package/src/commands/incentive.js +373 -0
  19. package/src/commands/inference.js +304 -0
  20. package/src/commands/infra.js +361 -0
  21. package/src/commands/ipfs.js +392 -0
  22. package/src/commands/kg.js +371 -0
  23. package/src/commands/marketplace.js +326 -0
  24. package/src/commands/mcp.js +97 -18
  25. package/src/commands/multimodal.js +404 -0
  26. package/src/commands/nlprog.js +329 -0
  27. package/src/commands/ops.js +408 -0
  28. package/src/commands/perception.js +385 -0
  29. package/src/commands/pqc.js +34 -0
  30. package/src/commands/privacy.js +345 -0
  31. package/src/commands/quantization.js +280 -0
  32. package/src/commands/recommend.js +336 -0
  33. package/src/commands/reputation.js +349 -0
  34. package/src/commands/runtime.js +500 -0
  35. package/src/commands/sla.js +352 -0
  36. package/src/commands/stress.js +252 -0
  37. package/src/commands/tech.js +268 -0
  38. package/src/commands/tenant.js +576 -0
  39. package/src/commands/trust.js +366 -0
  40. package/src/harness/mcp-client.js +330 -54
  41. package/src/index.js +118 -0
  42. package/src/lib/aiops.js +523 -0
  43. package/src/lib/autonomous-developer.js +524 -0
  44. package/src/lib/code-agent.js +442 -0
  45. package/src/lib/collaboration-governance.js +556 -0
  46. package/src/lib/community-governance.js +649 -0
  47. package/src/lib/content-recommendation.js +600 -0
  48. package/src/lib/cross-chain.js +669 -0
  49. package/src/lib/dbevo.js +669 -0
  50. package/src/lib/decentral-infra.js +445 -0
  51. package/src/lib/federation-hardening.js +587 -0
  52. package/src/lib/hardening-manager.js +409 -0
  53. package/src/lib/inference-network.js +407 -0
  54. package/src/lib/ipfs-storage.js +575 -0
  55. package/src/lib/knowledge-graph.js +530 -0
  56. package/src/lib/mcp-client.js +3 -0
  57. package/src/lib/multimodal.js +725 -0
  58. package/src/lib/nl-programming.js +595 -0
  59. package/src/lib/perception.js +500 -0
  60. package/src/lib/pqc-manager.js +141 -9
  61. package/src/lib/privacy-computing.js +575 -0
  62. package/src/lib/protocol-fusion.js +535 -0
  63. package/src/lib/quantization.js +362 -0
  64. package/src/lib/reputation-optimizer.js +509 -0
  65. package/src/lib/skill-marketplace.js +397 -0
  66. package/src/lib/sla-manager.js +484 -0
  67. package/src/lib/stress-tester.js +383 -0
  68. package/src/lib/tech-learning-engine.js +651 -0
  69. package/src/lib/tenant-saas.js +831 -0
  70. package/src/lib/token-incentive.js +513 -0
  71. package/src/lib/trust-security.js +473 -0
  72. package/src/lib/universal-runtime.js +771 -0
  73. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -0,0 +1,669 @@
1
+ /**
2
+ * Database Evolution Framework — CLI port of Phase 80
3
+ * (docs/design/modules/45_数据库演进与迁移框架.md).
4
+ *
5
+ * Desktop uses MigrationManager (up/down versioned migrations),
6
+ * QueryBuilder (fluent SQL), and IndexOptimizer (slow-query analysis).
7
+ * CLI port ships:
8
+ *
9
+ * - Migration registration, execution (up/down), rollback, history
10
+ * - Query logging with duration tracking and statistics
11
+ * - Index suggestion heuristics (slow-query analysis, table/column extraction)
12
+ * - Index suggestion application tracking
13
+ *
14
+ * What does NOT port: fluent QueryBuilder (runtime API, not CLI-facing),
15
+ * auto-migration on startup, real EXPLAIN-based optimization,
16
+ * periodic background analysis, database backup before migration.
17
+ */
18
+
19
+ import crypto from "crypto";
20
+
21
+ /* ── Constants ──────────────────────────────────────────── */
22
+
23
+ export const MIGRATION_STATUS = Object.freeze({
24
+ SUCCESS: "success",
25
+ FAILED: "failed",
26
+ ROLLED_BACK: "rolled_back",
27
+ });
28
+
29
+ export const MIGRATION_DIRECTION = Object.freeze({
30
+ UP: "up",
31
+ DOWN: "down",
32
+ });
33
+
34
+ export const SUGGESTION_TYPE = Object.freeze({
35
+ CREATE_INDEX: "create_index",
36
+ COMPOSITE_INDEX: "composite_index",
37
+ COVERING_INDEX: "covering_index",
38
+ });
39
+
40
+ /* ── State ──────────────────────────────────────────────── */
41
+
42
+ let _migrations = []; // registered migration definitions
43
+ let _migrationHistory = []; // executed migration records from DB
44
+ let _queryLogs = [];
45
+ let _suggestions = new Map();
46
+ let _slowQueryThresholdMs = 100;
47
+
48
+ function _id() {
49
+ return crypto.randomUUID();
50
+ }
51
+ function _now() {
52
+ return Date.now();
53
+ }
54
+
55
+ function _strip(row) {
56
+ if (!row) return null;
57
+ const out = {};
58
+ for (const [k, v] of Object.entries(row)) {
59
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /* ── Schema ─────────────────────────────────────────────── */
65
+
66
+ export function ensureDbEvoTables(db) {
67
+ db.exec(`CREATE TABLE IF NOT EXISTS _migrations (
68
+ id TEXT PRIMARY KEY,
69
+ version TEXT NOT NULL,
70
+ description TEXT,
71
+ direction TEXT NOT NULL,
72
+ executed_at INTEGER NOT NULL,
73
+ duration_ms INTEGER,
74
+ checksum TEXT,
75
+ status TEXT DEFAULT 'success'
76
+ )`);
77
+
78
+ db.exec(`CREATE TABLE IF NOT EXISTS _query_log (
79
+ id TEXT PRIMARY KEY,
80
+ sql_text TEXT NOT NULL,
81
+ params_json TEXT,
82
+ duration_ms REAL NOT NULL,
83
+ source TEXT,
84
+ tables_accessed TEXT,
85
+ created_at INTEGER NOT NULL
86
+ )`);
87
+
88
+ db.exec(`CREATE TABLE IF NOT EXISTS _index_suggestions (
89
+ id TEXT PRIMARY KEY,
90
+ table_name TEXT NOT NULL,
91
+ columns TEXT NOT NULL,
92
+ suggestion_type TEXT,
93
+ estimated_improvement REAL,
94
+ query_count INTEGER DEFAULT 0,
95
+ applied INTEGER DEFAULT 0,
96
+ created_at INTEGER
97
+ )`);
98
+
99
+ _loadAll(db);
100
+ }
101
+
102
+ function _loadAll(db) {
103
+ _migrationHistory = [];
104
+ _queryLogs = [];
105
+ _suggestions.clear();
106
+
107
+ const sources = [
108
+ ["_migrations", (r) => _migrationHistory.push(r)],
109
+ ["_query_log", (r) => _queryLogs.push(r)],
110
+ ["_index_suggestions", (r) => _suggestions.set(r.id, r)],
111
+ ];
112
+ for (const [table, handler] of sources) {
113
+ try {
114
+ for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
115
+ handler(_strip(row));
116
+ }
117
+ } catch (_e) {
118
+ /* table may not exist */
119
+ }
120
+ }
121
+ }
122
+
123
+ /* ── Migration Registration ─────────────────────────────── */
124
+
125
+ export function registerMigration(
126
+ version,
127
+ { description, upSql, downSql } = {},
128
+ ) {
129
+ if (!version) return { registered: false, reason: "missing_version" };
130
+ if (!upSql) return { registered: false, reason: "missing_up_sql" };
131
+
132
+ const existing = _migrations.find((m) => m.version === version);
133
+ if (existing) return { registered: false, reason: "duplicate_version" };
134
+
135
+ const checksum = crypto
136
+ .createHash("sha256")
137
+ .update(upSql + (downSql || ""))
138
+ .digest("hex")
139
+ .slice(0, 16);
140
+
141
+ _migrations.push({
142
+ version,
143
+ description: description || null,
144
+ upSql,
145
+ downSql: downSql || null,
146
+ checksum,
147
+ });
148
+
149
+ // Keep sorted by version
150
+ _migrations.sort((a, b) => a.version.localeCompare(b.version));
151
+
152
+ return { registered: true, version, checksum };
153
+ }
154
+
155
+ export function listRegisteredMigrations() {
156
+ return _migrations.map((m) => ({
157
+ version: m.version,
158
+ description: m.description,
159
+ checksum: m.checksum,
160
+ hasDown: !!m.downSql,
161
+ }));
162
+ }
163
+
164
+ /* ── Migration Execution ────────────────────────────────── */
165
+
166
+ export function getCurrentVersion(db) {
167
+ // Find the latest successful "up" migration that hasn't been rolled back
168
+ const ups = _migrationHistory
169
+ .filter((h) => h.direction === "up" && h.status === "success")
170
+ .map((h) => h.version);
171
+ const downs = _migrationHistory
172
+ .filter((h) => h.direction === "down" && h.status === "success")
173
+ .map((h) => h.version);
174
+
175
+ // Versions that have been migrated up and not rolled back
176
+ const active = ups.filter(
177
+ (v) => !downs.includes(v) || ups.lastIndexOf(v) > downs.lastIndexOf(v),
178
+ );
179
+
180
+ if (active.length === 0) return null;
181
+ return active.sort((a, b) => b.localeCompare(a))[0];
182
+ }
183
+
184
+ export function getPendingMigrations(db) {
185
+ const current = getCurrentVersion(db);
186
+ return _migrations.filter(
187
+ (m) => !current || m.version.localeCompare(current) > 0,
188
+ );
189
+ }
190
+
191
+ export function migrateUp(db, targetVersion) {
192
+ const pending = getPendingMigrations(db);
193
+ if (pending.length === 0) return { migrated: false, reason: "no_pending" };
194
+
195
+ const toRun = targetVersion
196
+ ? pending.filter((m) => m.version.localeCompare(targetVersion) <= 0)
197
+ : pending;
198
+
199
+ if (toRun.length === 0) return { migrated: false, reason: "no_pending" };
200
+
201
+ const results = [];
202
+ for (const migration of toRun) {
203
+ const start = _now();
204
+ const id = _id();
205
+ let status = "success";
206
+
207
+ // In CLI port, we record the migration as executed (no real SQL execution)
208
+ const durationMs = _now() - start;
209
+
210
+ const record = {
211
+ id,
212
+ version: migration.version,
213
+ description: migration.description,
214
+ direction: "up",
215
+ executed_at: _now(),
216
+ duration_ms: durationMs,
217
+ checksum: migration.checksum,
218
+ status,
219
+ };
220
+
221
+ db.prepare(
222
+ `INSERT INTO _migrations (id, version, description, direction, executed_at, duration_ms, checksum, status)
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
224
+ ).run(
225
+ id,
226
+ record.version,
227
+ record.description,
228
+ "up",
229
+ record.executed_at,
230
+ durationMs,
231
+ record.checksum,
232
+ status,
233
+ );
234
+
235
+ _migrationHistory.push(record);
236
+ results.push({ version: migration.version, status });
237
+ }
238
+
239
+ return { migrated: true, count: results.length, results };
240
+ }
241
+
242
+ export function migrateDown(db, targetVersion) {
243
+ const current = getCurrentVersion(db);
244
+ if (!current) return { rolledBack: false, reason: "no_current_version" };
245
+
246
+ // Find migrations to roll back (from current down to target, exclusive)
247
+ const toRollBack = _migrations
248
+ .filter((m) => {
249
+ if (targetVersion) {
250
+ return (
251
+ m.version.localeCompare(current) <= 0 &&
252
+ m.version.localeCompare(targetVersion) > 0
253
+ );
254
+ }
255
+ return m.version === current;
256
+ })
257
+ .sort((a, b) => b.version.localeCompare(a.version));
258
+
259
+ if (toRollBack.length === 0)
260
+ return { rolledBack: false, reason: "nothing_to_rollback" };
261
+
262
+ const noDown = toRollBack.filter((m) => !m.downSql);
263
+ if (noDown.length > 0) {
264
+ return {
265
+ rolledBack: false,
266
+ reason: "missing_down_migration",
267
+ versions: noDown.map((m) => m.version),
268
+ };
269
+ }
270
+
271
+ const results = [];
272
+ for (const migration of toRollBack) {
273
+ const id = _id();
274
+ const now = _now();
275
+
276
+ const record = {
277
+ id,
278
+ version: migration.version,
279
+ description: migration.description,
280
+ direction: "down",
281
+ executed_at: now,
282
+ duration_ms: 0,
283
+ checksum: migration.checksum,
284
+ status: "success",
285
+ };
286
+
287
+ db.prepare(
288
+ `INSERT INTO _migrations (id, version, description, direction, executed_at, duration_ms, checksum, status)
289
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
290
+ ).run(
291
+ id,
292
+ record.version,
293
+ record.description,
294
+ "down",
295
+ now,
296
+ 0,
297
+ record.checksum,
298
+ "success",
299
+ );
300
+
301
+ _migrationHistory.push(record);
302
+ results.push({ version: migration.version, status: "success" });
303
+ }
304
+
305
+ return { rolledBack: true, count: results.length, results };
306
+ }
307
+
308
+ export function getMigrationHistory(db, { limit = 50 } = {}) {
309
+ return _migrationHistory
310
+ .sort((a, b) => b.executed_at - a.executed_at)
311
+ .slice(0, limit)
312
+ .map((h) => ({ ...h }));
313
+ }
314
+
315
+ export function getMigrationStatus(db) {
316
+ const current = getCurrentVersion(db);
317
+ const pending = getPendingMigrations(db);
318
+ const history = getMigrationHistory(db, { limit: 5 });
319
+
320
+ return {
321
+ currentVersion: current,
322
+ pendingCount: pending.length,
323
+ pendingVersions: pending.map((m) => m.version),
324
+ totalRegistered: _migrations.length,
325
+ totalExecuted: _migrationHistory.length,
326
+ recentHistory: history,
327
+ };
328
+ }
329
+
330
+ export function validateMigrations() {
331
+ if (_migrations.length === 0) return { valid: true, issues: [] };
332
+
333
+ const issues = [];
334
+ const versions = _migrations.map((m) => m.version).sort();
335
+
336
+ // Check for gaps in version sequence
337
+ for (let i = 1; i < versions.length; i++) {
338
+ const prev = versions[i - 1];
339
+ const curr = versions[i];
340
+ // Simple gap detection: if versions are numeric-like, check continuity
341
+ const prevNum = parseInt(prev.replace(/\D/g, ""), 10);
342
+ const currNum = parseInt(curr.replace(/\D/g, ""), 10);
343
+ if (!isNaN(prevNum) && !isNaN(currNum) && currNum - prevNum > 1) {
344
+ issues.push({ type: "gap", between: [prev, curr] });
345
+ }
346
+ }
347
+
348
+ // Check for missing down migrations
349
+ for (const m of _migrations) {
350
+ if (!m.downSql) {
351
+ issues.push({ type: "missing_down", version: m.version });
352
+ }
353
+ }
354
+
355
+ return { valid: issues.length === 0, issues };
356
+ }
357
+
358
+ /* ── Query Logging ──────────────────────────────────────── */
359
+
360
+ export function logQuery(db, sqlText, durationMs, { params, source } = {}) {
361
+ if (!sqlText) return { logged: false, reason: "missing_sql" };
362
+ if (durationMs == null || durationMs < 0)
363
+ return { logged: false, reason: "invalid_duration" };
364
+
365
+ const id = _id();
366
+ const now = _now();
367
+
368
+ // Extract table names from SQL
369
+ const tableMatches =
370
+ sqlText.match(/(?:FROM|JOIN|INTO|UPDATE)\s+(\w+)/gi) || [];
371
+ const tables = [...new Set(tableMatches.map((m) => m.split(/\s+/).pop()))];
372
+ const tablesAccessed = tables.join(",");
373
+
374
+ const record = {
375
+ id,
376
+ sql_text: sqlText,
377
+ params_json: params ? JSON.stringify(params) : null,
378
+ duration_ms: durationMs,
379
+ source: source || null,
380
+ tables_accessed: tablesAccessed,
381
+ created_at: now,
382
+ };
383
+
384
+ db.prepare(
385
+ `INSERT INTO _query_log (id, sql_text, params_json, duration_ms, source, tables_accessed, created_at)
386
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
387
+ ).run(
388
+ id,
389
+ record.sql_text,
390
+ record.params_json,
391
+ durationMs,
392
+ record.source,
393
+ tablesAccessed,
394
+ now,
395
+ );
396
+
397
+ _queryLogs.push(record);
398
+
399
+ return { logged: true, id, isSlow: durationMs >= _slowQueryThresholdMs };
400
+ }
401
+
402
+ export function getQueryStats(db) {
403
+ if (_queryLogs.length === 0) {
404
+ return {
405
+ totalQueries: 0,
406
+ slowQueries: 0,
407
+ avgDurationMs: 0,
408
+ maxDurationMs: 0,
409
+ slowQueryThresholdMs: _slowQueryThresholdMs,
410
+ topSlow: [],
411
+ };
412
+ }
413
+
414
+ const durations = _queryLogs.map((q) => q.duration_ms);
415
+ const slow = _queryLogs.filter((q) => q.duration_ms >= _slowQueryThresholdMs);
416
+ const avg =
417
+ Math.round(
418
+ (durations.reduce((s, d) => s + d, 0) / durations.length) * 100,
419
+ ) / 100;
420
+ const max = Math.max(...durations);
421
+
422
+ // Top 10 slowest queries
423
+ const topSlow = [..._queryLogs]
424
+ .sort((a, b) => b.duration_ms - a.duration_ms)
425
+ .slice(0, 10)
426
+ .map((q) => ({
427
+ sql: q.sql_text.slice(0, 100),
428
+ durationMs: q.duration_ms,
429
+ tables: q.tables_accessed,
430
+ source: q.source,
431
+ }));
432
+
433
+ return {
434
+ totalQueries: _queryLogs.length,
435
+ slowQueries: slow.length,
436
+ avgDurationMs: avg,
437
+ maxDurationMs: max,
438
+ slowQueryThresholdMs: _slowQueryThresholdMs,
439
+ topSlow,
440
+ };
441
+ }
442
+
443
+ export function setSlowQueryThreshold(ms) {
444
+ if (ms == null || ms < 0) return { set: false, reason: "invalid_threshold" };
445
+ _slowQueryThresholdMs = ms;
446
+ return { set: true, thresholdMs: ms };
447
+ }
448
+
449
+ export function clearQueryLog(db) {
450
+ db.prepare("DELETE FROM _query_log").run();
451
+ const count = _queryLogs.length;
452
+ _queryLogs = [];
453
+ return { cleared: true, count };
454
+ }
455
+
456
+ /* ── Index Optimization ─────────────────────────────────── */
457
+
458
+ /**
459
+ * Analyze query logs to generate index suggestions.
460
+ * Heuristic: extract WHERE-clause columns from slow queries,
461
+ * count frequency per table+column, suggest indexes for frequent patterns.
462
+ */
463
+ export function analyzeQueries(db, { minQueryCount = 2 } = {}) {
464
+ const slow = _queryLogs.filter((q) => q.duration_ms >= _slowQueryThresholdMs);
465
+ if (slow.length === 0) return { analyzed: true, suggestionsGenerated: 0 };
466
+
467
+ // Extract WHERE columns per table
468
+ const tableColumnCounts = new Map(); // "table:col" → count
469
+
470
+ for (const q of slow) {
471
+ const tables = (q.tables_accessed || "").split(",").filter(Boolean);
472
+ if (tables.length === 0) continue;
473
+
474
+ // Extract column names from WHERE clauses
475
+ const whereMatch = q.sql_text.match(
476
+ /WHERE\s+(.+?)(?:\s+ORDER|\s+GROUP|\s+LIMIT|$)/is,
477
+ );
478
+ if (!whereMatch) continue;
479
+
480
+ const wherePart = whereMatch[1];
481
+ const colMatches =
482
+ wherePart.match(/(\w+)\s*(?:=|>|<|>=|<=|LIKE|IN)\s*/gi) || [];
483
+ const cols = colMatches
484
+ .map((m) => m.replace(/\s*(?:=|>|<|>=|<=|LIKE|IN)\s*/i, "").trim())
485
+ .filter(Boolean);
486
+
487
+ const primaryTable = tables[0];
488
+ for (const col of cols) {
489
+ const key = `${primaryTable}:${col}`;
490
+ tableColumnCounts.set(key, (tableColumnCounts.get(key) || 0) + 1);
491
+ }
492
+ }
493
+
494
+ // Generate suggestions for columns that appear at least minQueryCount times
495
+ let generated = 0;
496
+ for (const [key, count] of tableColumnCounts) {
497
+ if (count < minQueryCount) continue;
498
+
499
+ const [tableName, column] = key.split(":");
500
+ const existingId = [..._suggestions.values()].find(
501
+ (s) => s.table_name === tableName && s.columns === column && !s.applied,
502
+ );
503
+ if (existingId) continue; // Don't duplicate
504
+
505
+ const id = _id();
506
+ const now = _now();
507
+ const estimatedImprovement = Math.min(
508
+ 0.9,
509
+ Math.round((count / slow.length) * 100) / 100,
510
+ );
511
+
512
+ const suggestion = {
513
+ id,
514
+ table_name: tableName,
515
+ columns: column,
516
+ suggestion_type: "create_index",
517
+ estimated_improvement: estimatedImprovement,
518
+ query_count: count,
519
+ applied: 0,
520
+ created_at: now,
521
+ };
522
+
523
+ db.prepare(
524
+ `INSERT INTO _index_suggestions (id, table_name, columns, suggestion_type, estimated_improvement, query_count, applied, created_at)
525
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
526
+ ).run(
527
+ id,
528
+ tableName,
529
+ column,
530
+ "create_index",
531
+ estimatedImprovement,
532
+ count,
533
+ 0,
534
+ now,
535
+ );
536
+
537
+ _suggestions.set(id, suggestion);
538
+ generated++;
539
+ }
540
+
541
+ // Detect composite index opportunities (2+ columns on same table)
542
+ const tableColumns = new Map(); // table → [cols]
543
+ for (const [key] of tableColumnCounts) {
544
+ const [table, col] = key.split(":");
545
+ if (!tableColumns.has(table)) tableColumns.set(table, []);
546
+ tableColumns.get(table).push(col);
547
+ }
548
+
549
+ for (const [table, cols] of tableColumns) {
550
+ if (cols.length >= 2) {
551
+ const compositeKey = cols.sort().join(",");
552
+ const existingComposite = [..._suggestions.values()].find(
553
+ (s) => s.table_name === table && s.columns === compositeKey,
554
+ );
555
+ if (!existingComposite) {
556
+ const id = _id();
557
+ const now = _now();
558
+ const totalCount = cols.reduce(
559
+ (s, c) => s + (tableColumnCounts.get(`${table}:${c}`) || 0),
560
+ 0,
561
+ );
562
+
563
+ const suggestion = {
564
+ id,
565
+ table_name: table,
566
+ columns: compositeKey,
567
+ suggestion_type: "composite_index",
568
+ estimated_improvement: Math.min(
569
+ 0.95,
570
+ Math.round((totalCount / slow.length) * 100) / 100,
571
+ ),
572
+ query_count: totalCount,
573
+ applied: 0,
574
+ created_at: now,
575
+ };
576
+
577
+ db.prepare(
578
+ `INSERT INTO _index_suggestions (id, table_name, columns, suggestion_type, estimated_improvement, query_count, applied, created_at)
579
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
580
+ ).run(
581
+ id,
582
+ table,
583
+ compositeKey,
584
+ "composite_index",
585
+ suggestion.estimated_improvement,
586
+ totalCount,
587
+ 0,
588
+ now,
589
+ );
590
+
591
+ _suggestions.set(id, suggestion);
592
+ generated++;
593
+ }
594
+ }
595
+ }
596
+
597
+ return {
598
+ analyzed: true,
599
+ suggestionsGenerated: generated,
600
+ slowQueriesAnalyzed: slow.length,
601
+ };
602
+ }
603
+
604
+ export function listSuggestions(db, { applied } = {}) {
605
+ let sugs = [..._suggestions.values()];
606
+ if (applied != null) {
607
+ sugs = sugs.filter((s) => (applied ? s.applied === 1 : s.applied === 0));
608
+ }
609
+ return sugs
610
+ .sort((a, b) => b.query_count - a.query_count)
611
+ .map((s) => ({ ...s }));
612
+ }
613
+
614
+ export function getSuggestion(db, id) {
615
+ const s = _suggestions.get(id);
616
+ return s ? { ...s } : null;
617
+ }
618
+
619
+ export function applySuggestion(db, id) {
620
+ const s = _suggestions.get(id);
621
+ if (!s) return { applied: false, reason: "not_found" };
622
+ if (s.applied) return { applied: false, reason: "already_applied" };
623
+
624
+ s.applied = 1;
625
+ db.prepare("UPDATE _index_suggestions SET applied = 1 WHERE id = ?").run(id);
626
+
627
+ return {
628
+ applied: true,
629
+ indexSql: `CREATE INDEX IF NOT EXISTS idx_${s.table_name}_${s.columns.replace(/,/g, "_")} ON ${s.table_name} (${s.columns})`,
630
+ };
631
+ }
632
+
633
+ /* ── Stats ──────────────────────────────────────────────── */
634
+
635
+ export function getDbEvoStats(db) {
636
+ const current = getCurrentVersion(db);
637
+ const pending = getPendingMigrations(db);
638
+ const slow = _queryLogs.filter((q) => q.duration_ms >= _slowQueryThresholdMs);
639
+ const sugs = [..._suggestions.values()];
640
+
641
+ return {
642
+ migrations: {
643
+ registered: _migrations.length,
644
+ executed: _migrationHistory.length,
645
+ currentVersion: current,
646
+ pending: pending.length,
647
+ },
648
+ queryLog: {
649
+ total: _queryLogs.length,
650
+ slowQueries: slow.length,
651
+ thresholdMs: _slowQueryThresholdMs,
652
+ },
653
+ suggestions: {
654
+ total: sugs.length,
655
+ pending: sugs.filter((s) => !s.applied).length,
656
+ applied: sugs.filter((s) => s.applied).length,
657
+ },
658
+ };
659
+ }
660
+
661
+ /* ── Reset (tests) ──────────────────────────────────────── */
662
+
663
+ export function _resetState() {
664
+ _migrations = [];
665
+ _migrationHistory = [];
666
+ _queryLogs = [];
667
+ _suggestions.clear();
668
+ _slowQueryThresholdMs = 100;
669
+ }