mdkg 0.1.6 → 0.1.8

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.
@@ -0,0 +1,640 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runProjectDbMigrations = runProjectDbMigrations;
7
+ exports.verifyProjectDb = verifyProjectDb;
8
+ exports.projectDbStats = projectDbStats;
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const project_db_1 = require("./project_db");
13
+ const version_1 = require("./version");
14
+ const atomic_1 = require("../util/atomic");
15
+ const errors_1 = require("../util/errors");
16
+ const FOUNDATION_MIGRATION_SQL = `
17
+ CREATE TABLE IF NOT EXISTS project_meta (
18
+ key TEXT PRIMARY KEY,
19
+ value TEXT NOT NULL
20
+ ) STRICT;
21
+ `;
22
+ const QUEUE_MIGRATION_SQL = `
23
+ CREATE TABLE IF NOT EXISTS project_queue_message (
24
+ queue_name TEXT NOT NULL,
25
+ message_id TEXT NOT NULL,
26
+ dedupe_key TEXT,
27
+ payload_json TEXT NOT NULL,
28
+ payload_hash TEXT NOT NULL,
29
+ status TEXT NOT NULL CHECK(status IN ('ready', 'leased', 'acked', 'dead_letter')),
30
+ available_at_ms INTEGER NOT NULL,
31
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK(attempt_count >= 0),
32
+ max_attempts INTEGER NOT NULL CHECK(max_attempts > 0),
33
+ lease_owner TEXT,
34
+ lease_deadline_ms INTEGER,
35
+ created_at_ms INTEGER NOT NULL,
36
+ updated_at_ms INTEGER NOT NULL,
37
+ last_error TEXT,
38
+ CHECK (
39
+ (status = 'leased' AND lease_owner IS NOT NULL AND lease_deadline_ms IS NOT NULL)
40
+ OR
41
+ (status <> 'leased' AND lease_owner IS NULL AND lease_deadline_ms IS NULL)
42
+ ),
43
+ PRIMARY KEY (queue_name, message_id)
44
+ ) STRICT;
45
+
46
+ CREATE UNIQUE INDEX IF NOT EXISTS project_queue_message_dedupe_unique
47
+ ON project_queue_message(queue_name, dedupe_key)
48
+ WHERE dedupe_key IS NOT NULL;
49
+
50
+ CREATE INDEX IF NOT EXISTS project_queue_message_ready_idx
51
+ ON project_queue_message(queue_name, status, available_at_ms, created_at_ms, message_id);
52
+
53
+ CREATE INDEX IF NOT EXISTS project_queue_message_lease_idx
54
+ ON project_queue_message(queue_name, status, lease_deadline_ms, created_at_ms, message_id);
55
+ `;
56
+ const BUILTIN_MIGRATIONS = [
57
+ {
58
+ ordinal: 1,
59
+ key: "mdkg.project_db.foundation.v1",
60
+ filename: "001_mdkg_project_db_foundation.sql",
61
+ sql: FOUNDATION_MIGRATION_SQL.trim(),
62
+ },
63
+ {
64
+ ordinal: 2,
65
+ key: "mdkg.project_db.queue.v1",
66
+ filename: "002_mdkg_project_db_queue.sql",
67
+ sql: QUEUE_MIGRATION_SQL.trim(),
68
+ },
69
+ ];
70
+ function loadDatabaseCtor() {
71
+ try {
72
+ const loaded = require("node:sqlite");
73
+ if (!loaded.DatabaseSync) {
74
+ throw new Error("node:sqlite DatabaseSync is unavailable");
75
+ }
76
+ return loaded.DatabaseSync;
77
+ }
78
+ catch (err) {
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ throw new Error(`node:sqlite is required for mdkg project DB migrations: ${message}`);
81
+ }
82
+ }
83
+ function toPosix(relativePath) {
84
+ return relativePath.split(path_1.default.sep).join("/");
85
+ }
86
+ function rel(root, filePath) {
87
+ return toPosix(path_1.default.relative(root, filePath));
88
+ }
89
+ function checksumMigration(migration) {
90
+ const payload = [
91
+ `ordinal:${migration.ordinal}`,
92
+ `key:${migration.key}`,
93
+ migration.sql.trim(),
94
+ ].join("\n");
95
+ return `sha256:${crypto_1.default.createHash("sha256").update(payload).digest("hex")}`;
96
+ }
97
+ function checksumContent(content) {
98
+ return `sha256:${crypto_1.default.createHash("sha256").update(content.trim()).digest("hex")}`;
99
+ }
100
+ function assertDirectory(root, dirPath, label) {
101
+ const relative = rel(root, dirPath);
102
+ if (!fs_1.default.existsSync(dirPath)) {
103
+ throw new errors_1.ValidationError(`${label} missing at ${relative}; run mdkg db init`);
104
+ }
105
+ if (!fs_1.default.statSync(dirPath).isDirectory()) {
106
+ throw new errors_1.ValidationError(`${relative} exists and is not a directory`);
107
+ }
108
+ }
109
+ function directoryCheck(root, dirPath, name) {
110
+ const relative = rel(root, dirPath);
111
+ if (!fs_1.default.existsSync(dirPath)) {
112
+ return {
113
+ name,
114
+ ok: false,
115
+ level: "fail",
116
+ path: relative,
117
+ detail: `${name} missing`,
118
+ errors: [`${relative} missing; run mdkg db init`],
119
+ warnings: [],
120
+ };
121
+ }
122
+ if (!fs_1.default.statSync(dirPath).isDirectory()) {
123
+ return {
124
+ name,
125
+ ok: false,
126
+ level: "fail",
127
+ path: relative,
128
+ detail: `${name} is not a directory`,
129
+ errors: [`${relative} exists and is not a directory`],
130
+ warnings: [],
131
+ };
132
+ }
133
+ return {
134
+ name,
135
+ ok: true,
136
+ level: "ok",
137
+ path: relative,
138
+ detail: `${name} exists`,
139
+ errors: [],
140
+ warnings: [],
141
+ };
142
+ }
143
+ function assertProjectDbReady(root, config) {
144
+ if (!config.db.enabled) {
145
+ throw new errors_1.ValidationError("project db is disabled; run mdkg db init first");
146
+ }
147
+ if (config.db.schema_version !== project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION) {
148
+ throw new errors_1.ValidationError(`unsupported project db schema_version ${config.db.schema_version}; supported ${project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION}`);
149
+ }
150
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
151
+ assertDirectory(root, layout.db, "project db root");
152
+ assertDirectory(root, layout.schema, "project db schema directory");
153
+ assertDirectory(root, layout.migrations, "project db migrations directory");
154
+ assertDirectory(root, layout.runtimeDir, "project db runtime directory");
155
+ assertDirectory(root, layout.stateDir, "project db state directory");
156
+ assertDirectory(root, layout.receipts, "project db receipts directory");
157
+ if (fs_1.default.existsSync(layout.runtimeFile) && fs_1.default.statSync(layout.runtimeFile).isDirectory()) {
158
+ throw new errors_1.ValidationError(`${rel(root, layout.runtimeFile)} exists and is not a file`);
159
+ }
160
+ }
161
+ function ensureMigrationFiles(root, config) {
162
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
163
+ const created = [];
164
+ const unchanged = [];
165
+ for (const migration of BUILTIN_MIGRATIONS) {
166
+ const filePath = path_1.default.join(layout.migrations, migration.filename);
167
+ const expectedContent = `${migration.sql.trim()}\n`;
168
+ const relative = rel(root, filePath);
169
+ if (fs_1.default.existsSync(filePath)) {
170
+ if (fs_1.default.statSync(filePath).isDirectory()) {
171
+ throw new errors_1.ValidationError(`${relative} exists and is not a file`);
172
+ }
173
+ const current = fs_1.default.readFileSync(filePath, "utf8");
174
+ if (checksumContent(current) !== checksumContent(expectedContent)) {
175
+ throw new errors_1.ValidationError(`migration file checksum drift at ${relative}`);
176
+ }
177
+ unchanged.push(relative);
178
+ continue;
179
+ }
180
+ (0, atomic_1.atomicWriteFile)(filePath, expectedContent);
181
+ created.push(relative);
182
+ }
183
+ return { created, unchanged };
184
+ }
185
+ function checkMigrationFiles(root, config) {
186
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
187
+ const errors = [];
188
+ for (const migration of BUILTIN_MIGRATIONS) {
189
+ const filePath = path_1.default.join(layout.migrations, migration.filename);
190
+ const relative = rel(root, filePath);
191
+ const expectedContent = `${migration.sql.trim()}\n`;
192
+ if (!fs_1.default.existsSync(filePath)) {
193
+ errors.push(`${relative} missing; run mdkg db migrate`);
194
+ continue;
195
+ }
196
+ if (fs_1.default.statSync(filePath).isDirectory()) {
197
+ errors.push(`${relative} exists and is not a file`);
198
+ continue;
199
+ }
200
+ const current = fs_1.default.readFileSync(filePath, "utf8");
201
+ if (checksumContent(current) !== checksumContent(expectedContent)) {
202
+ errors.push(`migration file checksum drift at ${relative}`);
203
+ }
204
+ }
205
+ return {
206
+ name: "migration-files",
207
+ ok: errors.length === 0,
208
+ level: errors.length === 0 ? "ok" : "fail",
209
+ path: rel(root, layout.migrations),
210
+ detail: errors.length === 0 ? "migration files match mdkg-owned foundation migrations" : "migration file issues found",
211
+ errors,
212
+ warnings: [],
213
+ };
214
+ }
215
+ function createMigrationTableSql(tableName) {
216
+ return `
217
+ CREATE TABLE IF NOT EXISTS ${tableName} (
218
+ migration_key TEXT PRIMARY KEY,
219
+ ordinal INTEGER NOT NULL UNIQUE,
220
+ checksum TEXT NOT NULL,
221
+ applied_at_ms INTEGER NOT NULL,
222
+ mdkg_version TEXT NOT NULL
223
+ ) STRICT;
224
+ `;
225
+ }
226
+ function readAppliedMigrations(db, tableName) {
227
+ const rows = db
228
+ .prepare(`SELECT migration_key, ordinal, checksum, applied_at_ms FROM ${tableName} ORDER BY ordinal ASC`)
229
+ .all();
230
+ const applied = new Map();
231
+ for (const row of rows) {
232
+ applied.set(String(row.migration_key), {
233
+ migration_key: String(row.migration_key),
234
+ ordinal: Number(row.ordinal),
235
+ checksum: String(row.checksum),
236
+ applied_at_ms: Number(row.applied_at_ms),
237
+ });
238
+ }
239
+ return applied;
240
+ }
241
+ function assertIntegrity(db) {
242
+ const row = db.prepare("PRAGMA integrity_check").get();
243
+ const value = String(Object.values(row ?? {})[0] ?? "");
244
+ if (value !== "ok") {
245
+ throw new errors_1.ValidationError(`project DB integrity check failed: ${value}`);
246
+ }
247
+ }
248
+ function integrityCheck(db) {
249
+ try {
250
+ const row = db.prepare("PRAGMA integrity_check").get();
251
+ const value = String(Object.values(row ?? {})[0] ?? "");
252
+ if (value !== "ok") {
253
+ return {
254
+ name: "sqlite-integrity",
255
+ ok: false,
256
+ level: "fail",
257
+ detail: `integrity check failed: ${value}`,
258
+ errors: [`project DB integrity check failed: ${value}`],
259
+ warnings: [],
260
+ };
261
+ }
262
+ return {
263
+ name: "sqlite-integrity",
264
+ ok: true,
265
+ level: "ok",
266
+ detail: "SQLite integrity check ok",
267
+ errors: [],
268
+ warnings: [],
269
+ };
270
+ }
271
+ catch (err) {
272
+ const message = err instanceof Error ? err.message : String(err);
273
+ return {
274
+ name: "sqlite-integrity",
275
+ ok: false,
276
+ level: "fail",
277
+ detail: "failed to read SQLite database",
278
+ errors: [`failed to read project DB: ${message}`],
279
+ warnings: [],
280
+ };
281
+ }
282
+ }
283
+ function quoteIdentifier(value) {
284
+ return `"${value.replace(/"/g, '""')}"`;
285
+ }
286
+ function tableExists(db, tableName) {
287
+ const row = db
288
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
289
+ .get(tableName);
290
+ return row !== undefined;
291
+ }
292
+ function migrationTableCheck(db, config) {
293
+ const errors = [];
294
+ if (!tableExists(db, config.db.migration_table)) {
295
+ errors.push(`migration table ${config.db.migration_table} missing; run mdkg db migrate`);
296
+ }
297
+ else {
298
+ const applied = readAppliedMigrations(db, config.db.migration_table);
299
+ const knownKeys = new Set(BUILTIN_MIGRATIONS.map((migration) => migration.key));
300
+ for (const appliedKey of applied.keys()) {
301
+ if (!knownKeys.has(appliedKey)) {
302
+ errors.push(`project DB contains unsupported migration ${appliedKey}`);
303
+ }
304
+ }
305
+ for (const migration of BUILTIN_MIGRATIONS) {
306
+ const row = applied.get(migration.key);
307
+ const checksum = checksumMigration(migration);
308
+ if (!row) {
309
+ errors.push(`migration ${migration.key} missing; run mdkg db migrate`);
310
+ continue;
311
+ }
312
+ if (row.ordinal !== migration.ordinal) {
313
+ errors.push(`migration order drift for ${migration.key}`);
314
+ }
315
+ if (row.checksum !== checksum) {
316
+ errors.push(`migration checksum drift for ${migration.key}`);
317
+ }
318
+ }
319
+ }
320
+ return {
321
+ name: "migrations",
322
+ ok: errors.length === 0,
323
+ level: errors.length === 0 ? "ok" : "fail",
324
+ detail: errors.length === 0 ? "migration metadata is current" : "migration metadata issues found",
325
+ errors,
326
+ warnings: [],
327
+ };
328
+ }
329
+ function transientFiles(root, runtimeFile) {
330
+ return ["-wal", "-shm", "-journal"]
331
+ .map((suffix) => {
332
+ const filePath = `${runtimeFile}${suffix}`;
333
+ return {
334
+ path: rel(root, filePath),
335
+ exists: fs_1.default.existsSync(filePath),
336
+ size: fs_1.default.existsSync(filePath) ? fs_1.default.statSync(filePath).size : 0,
337
+ };
338
+ })
339
+ .filter((item) => item.exists);
340
+ }
341
+ function transientStateCheck(root, runtimeFile) {
342
+ const present = transientFiles(root, runtimeFile);
343
+ const warnings = present.map((item) => `active transient SQLite file present: ${item.path}`);
344
+ return {
345
+ name: "transient-runtime-files",
346
+ ok: true,
347
+ level: warnings.length > 0 ? "warn" : "ok",
348
+ detail: warnings.length > 0 ? "active transient runtime files are present" : "no active transient runtime files found",
349
+ errors: [],
350
+ warnings,
351
+ };
352
+ }
353
+ function walkFiles(root) {
354
+ if (!fs_1.default.existsSync(root)) {
355
+ return [];
356
+ }
357
+ const entries = fs_1.default.readdirSync(root, { withFileTypes: true });
358
+ const files = [];
359
+ for (const entry of entries) {
360
+ const fullPath = path_1.default.join(root, entry.name);
361
+ if (entry.isDirectory()) {
362
+ files.push(...walkFiles(fullPath));
363
+ }
364
+ else if (entry.isFile()) {
365
+ files.push(fullPath);
366
+ }
367
+ }
368
+ return files;
369
+ }
370
+ function tableCounts(db) {
371
+ const rows = db
372
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name ASC")
373
+ .all();
374
+ return rows.map((row) => {
375
+ const name = String(row.name);
376
+ const countRow = db.prepare(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(name)}`).get();
377
+ return { name, row_count: Number(countRow?.count ?? 0) };
378
+ });
379
+ }
380
+ function syncProjectMeta(db, config) {
381
+ const upsert = db.prepare("INSERT INTO project_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value WHERE value <> excluded.value");
382
+ upsert.run("tool", "mdkg");
383
+ upsert.run("schema_version", String(config.db.schema_version));
384
+ upsert.run("mdkg_version", (0, version_1.readPackageVersion)());
385
+ upsert.run("migration_table", config.db.migration_table);
386
+ upsert.run("last_migration_key", BUILTIN_MIGRATIONS[BUILTIN_MIGRATIONS.length - 1].key);
387
+ }
388
+ function runProjectDbMigrations(root, config) {
389
+ assertProjectDbReady(root, config);
390
+ const migrationFiles = ensureMigrationFiles(root, config);
391
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
392
+ fs_1.default.mkdirSync(path_1.default.dirname(layout.runtimeFile), { recursive: true });
393
+ const DatabaseSync = loadDatabaseCtor();
394
+ const db = new DatabaseSync(layout.runtimeFile);
395
+ const statuses = [];
396
+ let appliedCount = 0;
397
+ try {
398
+ db.exec("PRAGMA foreign_keys = ON;");
399
+ db.exec("PRAGMA synchronous = FULL;");
400
+ assertIntegrity(db);
401
+ db.exec(createMigrationTableSql(config.db.migration_table));
402
+ db.exec("BEGIN IMMEDIATE");
403
+ try {
404
+ const applied = readAppliedMigrations(db, config.db.migration_table);
405
+ const knownKeys = new Set(BUILTIN_MIGRATIONS.map((migration) => migration.key));
406
+ for (const appliedKey of applied.keys()) {
407
+ if (!knownKeys.has(appliedKey)) {
408
+ throw new errors_1.ValidationError(`project DB contains unsupported migration ${appliedKey}`);
409
+ }
410
+ }
411
+ const now = Date.now();
412
+ for (const migration of BUILTIN_MIGRATIONS) {
413
+ const checksum = checksumMigration(migration);
414
+ const row = applied.get(migration.key);
415
+ if (row) {
416
+ if (row.ordinal !== migration.ordinal) {
417
+ throw new errors_1.ValidationError(`migration order drift for ${migration.key}`);
418
+ }
419
+ if (row.checksum !== checksum) {
420
+ throw new errors_1.ValidationError(`migration checksum drift for ${migration.key}`);
421
+ }
422
+ statuses.push({
423
+ key: migration.key,
424
+ ordinal: migration.ordinal,
425
+ checksum,
426
+ status: "already_applied",
427
+ applied_at_ms: row.applied_at_ms,
428
+ });
429
+ continue;
430
+ }
431
+ db.exec(migration.sql);
432
+ const appliedAt = now + appliedCount;
433
+ db
434
+ .prepare(`INSERT INTO ${config.db.migration_table} (migration_key, ordinal, checksum, applied_at_ms, mdkg_version) VALUES (?, ?, ?, ?, ?)`)
435
+ .run(migration.key, migration.ordinal, checksum, appliedAt, (0, version_1.readPackageVersion)());
436
+ statuses.push({
437
+ key: migration.key,
438
+ ordinal: migration.ordinal,
439
+ checksum,
440
+ status: "applied",
441
+ applied_at_ms: appliedAt,
442
+ });
443
+ appliedCount += 1;
444
+ }
445
+ syncProjectMeta(db, config);
446
+ db.exec("COMMIT");
447
+ }
448
+ catch (err) {
449
+ try {
450
+ db.exec("ROLLBACK");
451
+ }
452
+ catch {
453
+ // ignore rollback failures when no transaction is active
454
+ }
455
+ throw err;
456
+ }
457
+ assertIntegrity(db);
458
+ }
459
+ catch (err) {
460
+ if (err instanceof errors_1.ValidationError) {
461
+ throw err;
462
+ }
463
+ const message = err instanceof Error ? err.message : String(err);
464
+ throw new errors_1.ValidationError(`project DB migration failed: ${message}`);
465
+ }
466
+ finally {
467
+ db.close();
468
+ }
469
+ return {
470
+ action: "db-migrate",
471
+ ok: true,
472
+ database: rel(root, layout.runtimeFile),
473
+ schema_version: config.db.schema_version,
474
+ migration_table: config.db.migration_table,
475
+ applied_count: appliedCount,
476
+ skipped_count: statuses.length - appliedCount,
477
+ migrations: statuses,
478
+ migration_files: {
479
+ created: migrationFiles.created.sort(),
480
+ unchanged: migrationFiles.unchanged.sort(),
481
+ },
482
+ };
483
+ }
484
+ function verifyProjectDb(root, config) {
485
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
486
+ const checks = [];
487
+ checks.push({
488
+ name: "config",
489
+ ok: config.db.enabled && config.db.schema_version === project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION,
490
+ level: config.db.enabled && config.db.schema_version === project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION ? "ok" : "fail",
491
+ detail: !config.db.enabled
492
+ ? "project db is disabled"
493
+ : config.db.schema_version === project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION
494
+ ? "project db config is enabled and supported"
495
+ : "project db schema version is unsupported",
496
+ errors: [
497
+ ...(!config.db.enabled ? ["project db is disabled; run mdkg db init"] : []),
498
+ ...(config.db.schema_version !== project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION
499
+ ? [`unsupported project db schema_version ${config.db.schema_version}; supported ${project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION}`]
500
+ : []),
501
+ ],
502
+ warnings: [],
503
+ });
504
+ checks.push(directoryCheck(root, layout.db, "project db root"));
505
+ checks.push(directoryCheck(root, layout.schema, "project db schema directory"));
506
+ checks.push(directoryCheck(root, layout.migrations, "project db migrations directory"));
507
+ checks.push(directoryCheck(root, layout.runtimeDir, "project db runtime directory"));
508
+ checks.push(directoryCheck(root, layout.stateDir, "project db state directory"));
509
+ checks.push(directoryCheck(root, layout.receipts, "project db receipts directory"));
510
+ const databasePath = rel(root, layout.runtimeFile);
511
+ if (!fs_1.default.existsSync(layout.runtimeFile)) {
512
+ checks.push({
513
+ name: "runtime-database",
514
+ ok: false,
515
+ level: "fail",
516
+ path: databasePath,
517
+ detail: "runtime database missing",
518
+ errors: [`${databasePath} missing; run mdkg db migrate`],
519
+ warnings: [],
520
+ });
521
+ }
522
+ else if (fs_1.default.statSync(layout.runtimeFile).isDirectory()) {
523
+ checks.push({
524
+ name: "runtime-database",
525
+ ok: false,
526
+ level: "fail",
527
+ path: databasePath,
528
+ detail: "runtime database path is not a file",
529
+ errors: [`${databasePath} exists and is not a file`],
530
+ warnings: [],
531
+ });
532
+ }
533
+ else {
534
+ checks.push({
535
+ name: "runtime-database",
536
+ ok: true,
537
+ level: "ok",
538
+ path: databasePath,
539
+ detail: "runtime database exists",
540
+ errors: [],
541
+ warnings: [],
542
+ });
543
+ }
544
+ checks.push(transientStateCheck(root, layout.runtimeFile));
545
+ const fatalBeforeOpen = checks.some((check) => !check.ok);
546
+ if (!fatalBeforeOpen && fs_1.default.existsSync(layout.runtimeFile)) {
547
+ checks.push(checkMigrationFiles(root, config));
548
+ const DatabaseSync = loadDatabaseCtor();
549
+ try {
550
+ const db = new DatabaseSync(layout.runtimeFile);
551
+ try {
552
+ checks.push(integrityCheck(db));
553
+ checks.push(migrationTableCheck(db, config));
554
+ }
555
+ finally {
556
+ db.close();
557
+ }
558
+ }
559
+ catch (err) {
560
+ const message = err instanceof Error ? err.message : String(err);
561
+ checks.push({
562
+ name: "sqlite-open",
563
+ ok: false,
564
+ level: "fail",
565
+ path: databasePath,
566
+ detail: "failed to open runtime database",
567
+ errors: [`failed to open project DB: ${message}`],
568
+ warnings: [],
569
+ });
570
+ }
571
+ }
572
+ const errors = checks.flatMap((check) => check.errors.map((error) => `${check.name}: ${error}`));
573
+ const warnings = checks.flatMap((check) => check.warnings.map((warning) => `${check.name}: ${warning}`));
574
+ return {
575
+ action: "db-verify",
576
+ ok: errors.length === 0,
577
+ enabled: config.db.enabled,
578
+ database: databasePath,
579
+ schema_version: config.db.schema_version,
580
+ migration_table: config.db.migration_table,
581
+ checks,
582
+ warning_count: warnings.length,
583
+ failure_count: errors.length,
584
+ warnings,
585
+ errors,
586
+ };
587
+ }
588
+ function projectDbStats(root, config) {
589
+ const verification = verifyProjectDb(root, config);
590
+ if (!verification.ok) {
591
+ throw new errors_1.ValidationError(`db stats requires a valid project DB; run mdkg db verify`);
592
+ }
593
+ const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
594
+ const DatabaseSync = loadDatabaseCtor();
595
+ const db = new DatabaseSync(layout.runtimeFile);
596
+ try {
597
+ const applied = readAppliedMigrations(db, config.db.migration_table);
598
+ const migrationStatuses = BUILTIN_MIGRATIONS
599
+ .map((migration) => {
600
+ const row = applied.get(migration.key);
601
+ return row
602
+ ? {
603
+ key: migration.key,
604
+ ordinal: migration.ordinal,
605
+ checksum: row.checksum,
606
+ status: "already_applied",
607
+ applied_at_ms: row.applied_at_ms,
608
+ }
609
+ : undefined;
610
+ })
611
+ .filter((item) => item !== undefined);
612
+ const latestMigration = migrationStatuses[migrationStatuses.length - 1] ?? null;
613
+ const receiptFiles = walkFiles(layout.receipts);
614
+ return {
615
+ action: "db-stats",
616
+ ok: true,
617
+ enabled: config.db.enabled,
618
+ database: rel(root, layout.runtimeFile),
619
+ schema_version: config.db.schema_version,
620
+ migration_table: config.db.migration_table,
621
+ db_size: fs_1.default.statSync(layout.runtimeFile).size,
622
+ transient_files: transientFiles(root, layout.runtimeFile),
623
+ migration_count: migrationStatuses.length,
624
+ latest_migration: latestMigration,
625
+ tables: tableCounts(db),
626
+ state_snapshot: {
627
+ path: rel(root, layout.stateFile),
628
+ exists: fs_1.default.existsSync(layout.stateFile),
629
+ size: fs_1.default.existsSync(layout.stateFile) ? fs_1.default.statSync(layout.stateFile).size : 0,
630
+ },
631
+ receipt_files: {
632
+ path: rel(root, layout.receipts),
633
+ count: receiptFiles.length,
634
+ },
635
+ };
636
+ }
637
+ finally {
638
+ db.close();
639
+ }
640
+ }