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