mdkg 0.1.5 → 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 +38 -2
- package/README.md +54 -3
- package/dist/cli.js +209 -4
- 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/task.js +2 -2
- 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 +14 -2
- 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,510 @@
|
|
|
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.resolveContainedProjectDbPath = resolveContainedProjectDbPath;
|
|
7
|
+
exports.sealProjectDbSnapshot = sealProjectDbSnapshot;
|
|
8
|
+
exports.verifyProjectDbSnapshot = verifyProjectDbSnapshot;
|
|
9
|
+
exports.projectDbSnapshotStatus = projectDbSnapshotStatus;
|
|
10
|
+
exports.canonicalDumpForSnapshot = canonicalDumpForSnapshot;
|
|
11
|
+
exports.dumpProjectDbSnapshot = dumpProjectDbSnapshot;
|
|
12
|
+
exports.diffProjectDbSnapshots = diffProjectDbSnapshots;
|
|
13
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const project_db_1 = require("./project_db");
|
|
17
|
+
const version_1 = require("./version");
|
|
18
|
+
const atomic_1 = require("../util/atomic");
|
|
19
|
+
const errors_1 = require("../util/errors");
|
|
20
|
+
const project_db_migrations_1 = require("./project_db_migrations");
|
|
21
|
+
function loadDatabaseCtor() {
|
|
22
|
+
try {
|
|
23
|
+
const loaded = require("node:sqlite");
|
|
24
|
+
if (!loaded.DatabaseSync) {
|
|
25
|
+
throw new Error("node:sqlite DatabaseSync is unavailable");
|
|
26
|
+
}
|
|
27
|
+
return loaded.DatabaseSync;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
throw new Error(`node:sqlite is required for mdkg project DB snapshots: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function toPosix(relativePath) {
|
|
35
|
+
return relativePath.split(path_1.default.sep).join("/");
|
|
36
|
+
}
|
|
37
|
+
function rel(root, filePath) {
|
|
38
|
+
return toPosix(path_1.default.relative(root, filePath));
|
|
39
|
+
}
|
|
40
|
+
function isInside(root, filePath) {
|
|
41
|
+
const relative = path_1.default.relative(root, filePath);
|
|
42
|
+
return relative === "" || (!relative.startsWith("..") && !path_1.default.isAbsolute(relative));
|
|
43
|
+
}
|
|
44
|
+
function resolveContainedProjectDbPath(root, rawPath, label) {
|
|
45
|
+
if (rawPath.trim().length === 0) {
|
|
46
|
+
throw new errors_1.UsageError(`${label} requires a non-empty path`);
|
|
47
|
+
}
|
|
48
|
+
const resolved = path_1.default.resolve(root, rawPath);
|
|
49
|
+
if (!isInside(root, resolved)) {
|
|
50
|
+
throw new errors_1.UsageError(`${label} must be inside the repo`);
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
function sha256Buffer(data) {
|
|
55
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(data).digest("hex")}`;
|
|
56
|
+
}
|
|
57
|
+
function sha256File(filePath) {
|
|
58
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(fs_1.default.readFileSync(filePath)).digest("hex")}`;
|
|
59
|
+
}
|
|
60
|
+
function quoteIdentifier(value) {
|
|
61
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
62
|
+
}
|
|
63
|
+
function quoteSqlString(value) {
|
|
64
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
65
|
+
}
|
|
66
|
+
function tableNames(db) {
|
|
67
|
+
return db
|
|
68
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name ASC")
|
|
69
|
+
.all()
|
|
70
|
+
.map((row) => String(row.name));
|
|
71
|
+
}
|
|
72
|
+
function tableCounts(db) {
|
|
73
|
+
return tableNames(db).map((name) => {
|
|
74
|
+
const row = db.prepare(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(name)}`).get();
|
|
75
|
+
return { name, row_count: Number(row?.count ?? 0) };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function readMigrations(db, tableName) {
|
|
79
|
+
const exists = db
|
|
80
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
81
|
+
.get(tableName);
|
|
82
|
+
if (!exists) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return db
|
|
86
|
+
.prepare(`SELECT migration_key, ordinal, checksum, applied_at_ms FROM ${quoteIdentifier(tableName)} ORDER BY ordinal ASC`)
|
|
87
|
+
.all()
|
|
88
|
+
.map((row) => ({
|
|
89
|
+
migration_key: String(row.migration_key),
|
|
90
|
+
ordinal: Number(row.ordinal),
|
|
91
|
+
checksum: String(row.checksum),
|
|
92
|
+
applied_at_ms: Number(row.applied_at_ms),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
function assertSqliteIntegrity(db, label) {
|
|
96
|
+
const row = db.prepare("PRAGMA integrity_check").get();
|
|
97
|
+
const value = String(Object.values(row ?? {})[0] ?? "");
|
|
98
|
+
if (value !== "ok") {
|
|
99
|
+
throw new errors_1.ValidationError(`${label} integrity check failed: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function sqliteIntegrityCheck(root, filePath) {
|
|
103
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
104
|
+
try {
|
|
105
|
+
const db = new DatabaseSync(filePath);
|
|
106
|
+
try {
|
|
107
|
+
assertSqliteIntegrity(db, "snapshot");
|
|
108
|
+
return {
|
|
109
|
+
name: "sqlite-integrity",
|
|
110
|
+
ok: true,
|
|
111
|
+
level: "ok",
|
|
112
|
+
path: rel(root, filePath),
|
|
113
|
+
detail: "snapshot SQLite integrity check ok",
|
|
114
|
+
errors: [],
|
|
115
|
+
warnings: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
db.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
return {
|
|
125
|
+
name: "sqlite-integrity",
|
|
126
|
+
ok: false,
|
|
127
|
+
level: "fail",
|
|
128
|
+
path: rel(root, filePath),
|
|
129
|
+
detail: "failed to verify snapshot SQLite integrity",
|
|
130
|
+
errors: [`snapshot SQLite integrity failed: ${message}`],
|
|
131
|
+
warnings: [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function collectSnapshotMetadata(filePath, migrationTable) {
|
|
136
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
137
|
+
const db = new DatabaseSync(filePath);
|
|
138
|
+
try {
|
|
139
|
+
assertSqliteIntegrity(db, "snapshot");
|
|
140
|
+
return {
|
|
141
|
+
table_counts: tableCounts(db),
|
|
142
|
+
migrations: readMigrations(db, migrationTable),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
db.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function readManifest(filePath) {
|
|
150
|
+
let parsed;
|
|
151
|
+
try {
|
|
152
|
+
parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
156
|
+
throw new errors_1.ValidationError(`failed to read snapshot manifest: ${message}`);
|
|
157
|
+
}
|
|
158
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
159
|
+
throw new errors_1.ValidationError("snapshot manifest must be a JSON object");
|
|
160
|
+
}
|
|
161
|
+
const manifest = parsed;
|
|
162
|
+
if (manifest.manifest_version !== 1 ||
|
|
163
|
+
manifest.tool !== "mdkg" ||
|
|
164
|
+
manifest.kind !== "project_db_snapshot" ||
|
|
165
|
+
typeof manifest.snapshot_sha256 !== "string" ||
|
|
166
|
+
typeof manifest.byte_size !== "number" ||
|
|
167
|
+
!Array.isArray(manifest.table_counts) ||
|
|
168
|
+
!Array.isArray(manifest.migrations)) {
|
|
169
|
+
throw new errors_1.ValidationError("snapshot manifest has unsupported shape");
|
|
170
|
+
}
|
|
171
|
+
return manifest;
|
|
172
|
+
}
|
|
173
|
+
function buildManifest(root, config, snapshotFile, runtimeHash) {
|
|
174
|
+
const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
|
|
175
|
+
const metadata = collectSnapshotMetadata(snapshotFile, config.db.migration_table);
|
|
176
|
+
return {
|
|
177
|
+
manifest_version: 1,
|
|
178
|
+
tool: "mdkg",
|
|
179
|
+
kind: "project_db_snapshot",
|
|
180
|
+
mdkg_version: (0, version_1.readPackageVersion)(),
|
|
181
|
+
generated_at: new Date().toISOString(),
|
|
182
|
+
schema_version: config.db.schema_version,
|
|
183
|
+
migration_table: config.db.migration_table,
|
|
184
|
+
runtime_path: rel(root, layout.runtimeFile),
|
|
185
|
+
snapshot_path: rel(root, layout.stateFile),
|
|
186
|
+
source_runtime_sha256: runtimeHash,
|
|
187
|
+
snapshot_sha256: sha256File(snapshotFile),
|
|
188
|
+
byte_size: fs_1.default.statSync(snapshotFile).size,
|
|
189
|
+
table_counts: metadata.table_counts,
|
|
190
|
+
migrations: metadata.migrations,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function warningListFromVerify(root, config) {
|
|
194
|
+
return (0, project_db_migrations_1.verifyProjectDb)(root, config).warnings;
|
|
195
|
+
}
|
|
196
|
+
function sealProjectDbSnapshot(root, config) {
|
|
197
|
+
const verification = (0, project_db_migrations_1.verifyProjectDb)(root, config);
|
|
198
|
+
if (!verification.ok) {
|
|
199
|
+
throw new errors_1.ValidationError(`db snapshot seal requires a valid project DB; run mdkg db verify`);
|
|
200
|
+
}
|
|
201
|
+
const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
|
|
202
|
+
const oldHash = fs_1.default.existsSync(layout.stateFile) ? sha256File(layout.stateFile) : null;
|
|
203
|
+
fs_1.default.mkdirSync(layout.stateDir, { recursive: true });
|
|
204
|
+
const tempSnapshot = path_1.default.join(layout.stateDir, `.project.sqlite.${process.pid}-${Date.now()}.tmp`);
|
|
205
|
+
const tempManifest = path_1.default.join(layout.stateDir, `.project.manifest.${process.pid}-${Date.now()}.tmp`);
|
|
206
|
+
fs_1.default.rmSync(tempSnapshot, { force: true });
|
|
207
|
+
fs_1.default.rmSync(tempManifest, { force: true });
|
|
208
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
209
|
+
const db = new DatabaseSync(layout.runtimeFile);
|
|
210
|
+
try {
|
|
211
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
212
|
+
assertSqliteIntegrity(db, "runtime project DB");
|
|
213
|
+
try {
|
|
214
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// WAL checkpoint can be a no-op or unavailable depending on journal mode.
|
|
218
|
+
}
|
|
219
|
+
db.exec(`VACUUM INTO ${quoteSqlString(tempSnapshot)}`);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
fs_1.default.rmSync(tempSnapshot, { force: true });
|
|
223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
+
throw err instanceof errors_1.ValidationError ? err : new errors_1.ValidationError(`db snapshot seal failed: ${message}`);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
db.close();
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const runtimeHash = sha256File(layout.runtimeFile);
|
|
231
|
+
const manifest = buildManifest(root, config, tempSnapshot, runtimeHash);
|
|
232
|
+
(0, atomic_1.atomicWriteFile)(tempManifest, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
233
|
+
fs_1.default.renameSync(tempSnapshot, layout.stateFile);
|
|
234
|
+
fs_1.default.renameSync(tempManifest, layout.stateManifest);
|
|
235
|
+
return {
|
|
236
|
+
action: "db-snapshot-seal",
|
|
237
|
+
ok: true,
|
|
238
|
+
snapshot: rel(root, layout.stateFile),
|
|
239
|
+
manifest: rel(root, layout.stateManifest),
|
|
240
|
+
old_snapshot_sha256: oldHash,
|
|
241
|
+
new_snapshot_sha256: manifest.snapshot_sha256,
|
|
242
|
+
source_runtime_sha256: runtimeHash,
|
|
243
|
+
byte_size: manifest.byte_size,
|
|
244
|
+
table_counts: manifest.table_counts,
|
|
245
|
+
migrations: manifest.migrations,
|
|
246
|
+
warnings: warningListFromVerify(root, config),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
fs_1.default.rmSync(tempSnapshot, { force: true });
|
|
251
|
+
fs_1.default.rmSync(tempManifest, { force: true });
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function compareJson(a, b) {
|
|
256
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
257
|
+
}
|
|
258
|
+
function verifyProjectDbSnapshot(root, config) {
|
|
259
|
+
const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
|
|
260
|
+
const checks = [];
|
|
261
|
+
const snapshotRel = rel(root, layout.stateFile);
|
|
262
|
+
const manifestRel = rel(root, layout.stateManifest);
|
|
263
|
+
checks.push({
|
|
264
|
+
name: "snapshot-file",
|
|
265
|
+
ok: fs_1.default.existsSync(layout.stateFile) && !fs_1.default.statSync(layout.stateFile).isDirectory(),
|
|
266
|
+
level: fs_1.default.existsSync(layout.stateFile) && !fs_1.default.statSync(layout.stateFile).isDirectory() ? "ok" : "fail",
|
|
267
|
+
path: snapshotRel,
|
|
268
|
+
detail: fs_1.default.existsSync(layout.stateFile) ? "snapshot file exists" : "snapshot file missing",
|
|
269
|
+
errors: fs_1.default.existsSync(layout.stateFile) ? [] : [`${snapshotRel} missing; run mdkg db snapshot seal`],
|
|
270
|
+
warnings: [],
|
|
271
|
+
});
|
|
272
|
+
checks.push({
|
|
273
|
+
name: "manifest-file",
|
|
274
|
+
ok: fs_1.default.existsSync(layout.stateManifest) && !fs_1.default.statSync(layout.stateManifest).isDirectory(),
|
|
275
|
+
level: fs_1.default.existsSync(layout.stateManifest) && !fs_1.default.statSync(layout.stateManifest).isDirectory() ? "ok" : "fail",
|
|
276
|
+
path: manifestRel,
|
|
277
|
+
detail: fs_1.default.existsSync(layout.stateManifest) ? "snapshot manifest exists" : "snapshot manifest missing",
|
|
278
|
+
errors: fs_1.default.existsSync(layout.stateManifest) ? [] : [`${manifestRel} missing; run mdkg db snapshot seal`],
|
|
279
|
+
warnings: [],
|
|
280
|
+
});
|
|
281
|
+
let manifest;
|
|
282
|
+
if (checks.every((check) => check.ok)) {
|
|
283
|
+
try {
|
|
284
|
+
manifest = readManifest(layout.stateManifest);
|
|
285
|
+
checks.push({
|
|
286
|
+
name: "manifest-shape",
|
|
287
|
+
ok: true,
|
|
288
|
+
level: "ok",
|
|
289
|
+
path: manifestRel,
|
|
290
|
+
detail: "snapshot manifest shape is supported",
|
|
291
|
+
errors: [],
|
|
292
|
+
warnings: [],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
297
|
+
checks.push({
|
|
298
|
+
name: "manifest-shape",
|
|
299
|
+
ok: false,
|
|
300
|
+
level: "fail",
|
|
301
|
+
path: manifestRel,
|
|
302
|
+
detail: "snapshot manifest shape is invalid",
|
|
303
|
+
errors: [message],
|
|
304
|
+
warnings: [],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (manifest) {
|
|
309
|
+
checks.push(sqliteIntegrityCheck(root, layout.stateFile));
|
|
310
|
+
const actualHash = sha256File(layout.stateFile);
|
|
311
|
+
const actualSize = fs_1.default.statSync(layout.stateFile).size;
|
|
312
|
+
checks.push({
|
|
313
|
+
name: "snapshot-hash",
|
|
314
|
+
ok: actualHash === manifest.snapshot_sha256,
|
|
315
|
+
level: actualHash === manifest.snapshot_sha256 ? "ok" : "fail",
|
|
316
|
+
path: snapshotRel,
|
|
317
|
+
detail: actualHash === manifest.snapshot_sha256 ? "snapshot hash matches manifest" : "snapshot hash mismatch",
|
|
318
|
+
errors: actualHash === manifest.snapshot_sha256 ? [] : [`snapshot hash mismatch: manifest ${manifest.snapshot_sha256}, actual ${actualHash}`],
|
|
319
|
+
warnings: [],
|
|
320
|
+
});
|
|
321
|
+
checks.push({
|
|
322
|
+
name: "snapshot-size",
|
|
323
|
+
ok: actualSize === manifest.byte_size,
|
|
324
|
+
level: actualSize === manifest.byte_size ? "ok" : "fail",
|
|
325
|
+
path: snapshotRel,
|
|
326
|
+
detail: actualSize === manifest.byte_size ? "snapshot byte size matches manifest" : "snapshot byte size mismatch",
|
|
327
|
+
errors: actualSize === manifest.byte_size ? [] : [`snapshot byte size mismatch: manifest ${manifest.byte_size}, actual ${actualSize}`],
|
|
328
|
+
warnings: [],
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
const metadata = collectSnapshotMetadata(layout.stateFile, config.db.migration_table);
|
|
332
|
+
const tablesMatch = compareJson(metadata.table_counts, manifest.table_counts);
|
|
333
|
+
checks.push({
|
|
334
|
+
name: "table-counts",
|
|
335
|
+
ok: tablesMatch,
|
|
336
|
+
level: tablesMatch ? "ok" : "fail",
|
|
337
|
+
detail: tablesMatch ? "table counts match manifest" : "table counts mismatch",
|
|
338
|
+
errors: tablesMatch ? [] : ["table counts do not match snapshot manifest"],
|
|
339
|
+
warnings: [],
|
|
340
|
+
});
|
|
341
|
+
const migrationsMatch = compareJson(metadata.migrations, manifest.migrations);
|
|
342
|
+
checks.push({
|
|
343
|
+
name: "migrations",
|
|
344
|
+
ok: migrationsMatch,
|
|
345
|
+
level: migrationsMatch ? "ok" : "fail",
|
|
346
|
+
detail: migrationsMatch ? "migration metadata matches manifest" : "migration metadata mismatch",
|
|
347
|
+
errors: migrationsMatch ? [] : ["migration metadata does not match snapshot manifest"],
|
|
348
|
+
warnings: [],
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
+
checks.push({
|
|
354
|
+
name: "snapshot-metadata",
|
|
355
|
+
ok: false,
|
|
356
|
+
level: "fail",
|
|
357
|
+
detail: "failed to read snapshot metadata",
|
|
358
|
+
errors: [`failed to read snapshot metadata: ${message}`],
|
|
359
|
+
warnings: [],
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (manifest.source_runtime_sha256 && fs_1.default.existsSync(layout.runtimeFile)) {
|
|
363
|
+
const runtimeHash = sha256File(layout.runtimeFile);
|
|
364
|
+
checks.push({
|
|
365
|
+
name: "runtime-freshness",
|
|
366
|
+
ok: true,
|
|
367
|
+
level: runtimeHash === manifest.source_runtime_sha256 ? "ok" : "warn",
|
|
368
|
+
path: rel(root, layout.runtimeFile),
|
|
369
|
+
detail: runtimeHash === manifest.source_runtime_sha256 ? "snapshot matches current runtime hash" : "runtime changed since snapshot seal",
|
|
370
|
+
errors: [],
|
|
371
|
+
warnings: runtimeHash === manifest.source_runtime_sha256 ? [] : ["runtime database hash differs from sealed snapshot source hash"],
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const errors = checks.flatMap((check) => check.errors.map((error) => `${check.name}: ${error}`));
|
|
376
|
+
const warnings = checks.flatMap((check) => check.warnings.map((warning) => `${check.name}: ${warning}`));
|
|
377
|
+
const hasMissing = checks.some((check) => !check.ok && /missing/.test(check.detail));
|
|
378
|
+
const stale = warnings.some((warning) => /runtime database hash differs/.test(warning));
|
|
379
|
+
return {
|
|
380
|
+
action: "db-snapshot-verify",
|
|
381
|
+
ok: errors.length === 0,
|
|
382
|
+
status: errors.length > 0 ? (hasMissing ? "missing" : "invalid") : stale ? "stale" : "valid",
|
|
383
|
+
snapshot: snapshotRel,
|
|
384
|
+
manifest: manifestRel,
|
|
385
|
+
checks,
|
|
386
|
+
warning_count: warnings.length,
|
|
387
|
+
failure_count: errors.length,
|
|
388
|
+
warnings,
|
|
389
|
+
errors,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function projectDbSnapshotStatus(root, config) {
|
|
393
|
+
const payload = verifyProjectDbSnapshot(root, config);
|
|
394
|
+
return {
|
|
395
|
+
...payload,
|
|
396
|
+
action: "db-snapshot-status",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function schemaLines(db) {
|
|
400
|
+
return db
|
|
401
|
+
.prepare("SELECT type, name, tbl_name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type ASC, name ASC")
|
|
402
|
+
.all()
|
|
403
|
+
.map((row) => `schema ${String(row.type)} ${String(row.name)}: ${String(row.sql).replace(/\s+/g, " ").trim()}`);
|
|
404
|
+
}
|
|
405
|
+
function columnNames(db, tableName) {
|
|
406
|
+
return db
|
|
407
|
+
.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`)
|
|
408
|
+
.all()
|
|
409
|
+
.map((row) => String(row.name));
|
|
410
|
+
}
|
|
411
|
+
function canonicalValue(value) {
|
|
412
|
+
if (value instanceof Uint8Array) {
|
|
413
|
+
const buffer = Buffer.from(value);
|
|
414
|
+
return {
|
|
415
|
+
blob_sha256: sha256Buffer(buffer),
|
|
416
|
+
byte_size: buffer.length,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
function canonicalDumpForSnapshot(root, snapshotPath) {
|
|
422
|
+
if (!fs_1.default.existsSync(snapshotPath) || fs_1.default.statSync(snapshotPath).isDirectory()) {
|
|
423
|
+
throw new errors_1.ValidationError(`${rel(root, snapshotPath)} missing or not a file`);
|
|
424
|
+
}
|
|
425
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
426
|
+
const db = new DatabaseSync(snapshotPath);
|
|
427
|
+
try {
|
|
428
|
+
assertSqliteIntegrity(db, "snapshot");
|
|
429
|
+
const lines = [
|
|
430
|
+
"# mdkg project db canonical dump v1",
|
|
431
|
+
`snapshot: ${rel(root, snapshotPath)}`,
|
|
432
|
+
`snapshot_sha256: ${sha256File(snapshotPath)}`,
|
|
433
|
+
"",
|
|
434
|
+
"# Schema",
|
|
435
|
+
...schemaLines(db),
|
|
436
|
+
"",
|
|
437
|
+
"# Tables",
|
|
438
|
+
];
|
|
439
|
+
for (const table of tableNames(db)) {
|
|
440
|
+
const columns = columnNames(db, table);
|
|
441
|
+
lines.push(`table ${table}`);
|
|
442
|
+
lines.push(`columns ${JSON.stringify(columns)}`);
|
|
443
|
+
const selectColumns = columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
444
|
+
const orderBy = columns.map((column) => `${quoteIdentifier(column)} ASC`).join(", ");
|
|
445
|
+
const rows = db
|
|
446
|
+
.prepare(`SELECT ${selectColumns} FROM ${quoteIdentifier(table)} ORDER BY ${orderBy}`)
|
|
447
|
+
.all();
|
|
448
|
+
for (const row of rows) {
|
|
449
|
+
const canonicalRow = {};
|
|
450
|
+
for (const column of columns) {
|
|
451
|
+
canonicalRow[column] = canonicalValue(row[column]);
|
|
452
|
+
}
|
|
453
|
+
lines.push(`row ${JSON.stringify(canonicalRow)}`);
|
|
454
|
+
}
|
|
455
|
+
lines.push("");
|
|
456
|
+
}
|
|
457
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
db.close();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function dumpProjectDbSnapshot(root, config, snapshotPath, outputPath) {
|
|
464
|
+
const layout = (0, project_db_1.resolveConfiguredProjectDbLayout)(root, config.db);
|
|
465
|
+
const resolvedSnapshot = snapshotPath
|
|
466
|
+
? resolveContainedProjectDbPath(root, snapshotPath, "--snapshot")
|
|
467
|
+
: layout.stateFile;
|
|
468
|
+
const dump = canonicalDumpForSnapshot(root, resolvedSnapshot);
|
|
469
|
+
const dumpHash = sha256Buffer(dump);
|
|
470
|
+
let output = null;
|
|
471
|
+
if (outputPath) {
|
|
472
|
+
const resolvedOutput = resolveContainedProjectDbPath(root, outputPath, "--output");
|
|
473
|
+
(0, atomic_1.atomicWriteFile)(resolvedOutput, dump);
|
|
474
|
+
output = rel(root, resolvedOutput);
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
action: "db-snapshot-dump",
|
|
478
|
+
ok: true,
|
|
479
|
+
snapshot: rel(root, resolvedSnapshot),
|
|
480
|
+
output,
|
|
481
|
+
line_count: dump.trimEnd().length === 0 ? 0 : dump.trimEnd().split("\n").length,
|
|
482
|
+
sha256: dumpHash,
|
|
483
|
+
dump,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function diffProjectDbSnapshots(root, leftPath, rightPath) {
|
|
487
|
+
const left = resolveContainedProjectDbPath(root, leftPath, "left snapshot");
|
|
488
|
+
const right = resolveContainedProjectDbPath(root, rightPath, "right snapshot");
|
|
489
|
+
const leftDump = canonicalDumpForSnapshot(root, left);
|
|
490
|
+
const rightDump = canonicalDumpForSnapshot(root, right);
|
|
491
|
+
const leftLines = leftDump.trimEnd().split("\n");
|
|
492
|
+
const rightLines = rightDump.trimEnd().split("\n");
|
|
493
|
+
const leftSet = new Set(leftLines);
|
|
494
|
+
const rightSet = new Set(rightLines);
|
|
495
|
+
const added = rightLines.filter((line) => !leftSet.has(line));
|
|
496
|
+
const removed = leftLines.filter((line) => !rightSet.has(line));
|
|
497
|
+
return {
|
|
498
|
+
action: "db-snapshot-diff",
|
|
499
|
+
ok: true,
|
|
500
|
+
left: rel(root, left),
|
|
501
|
+
right: rel(root, right),
|
|
502
|
+
left_sha256: sha256Buffer(leftDump),
|
|
503
|
+
right_sha256: sha256Buffer(rightDump),
|
|
504
|
+
added_count: added.length,
|
|
505
|
+
removed_count: removed.length,
|
|
506
|
+
changed_count: added.length + removed.length,
|
|
507
|
+
added,
|
|
508
|
+
removed,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
@@ -250,6 +250,11 @@ function requirePortableId(value, key, filePath) {
|
|
|
250
250
|
throw formatError(filePath, `${key} must be a lowercase portable id`);
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
+
function requirePortableIdRef(value, key, filePath) {
|
|
254
|
+
if (value !== value.toLowerCase() || !(0, id_1.isPortableIdRef)(value)) {
|
|
255
|
+
throw formatError(filePath, `${key} must be a lowercase portable id or qid`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
253
258
|
function requireSemver(value, key, filePath) {
|
|
254
259
|
if (!SEMVER_RE.test(value)) {
|
|
255
260
|
throw formatError(filePath, `${key} must be a semantic version like 1.0.0`);
|
|
@@ -280,8 +285,8 @@ function validatePortableRefs(values, key, filePath) {
|
|
|
280
285
|
if (value !== value.toLowerCase()) {
|
|
281
286
|
throw formatError(filePath, `${key}[${index}] must be lowercase`);
|
|
282
287
|
}
|
|
283
|
-
if (!(0, id_1.
|
|
284
|
-
throw formatError(filePath, `${key}[${index}] must be a lowercase portable id`);
|
|
288
|
+
if (!(0, id_1.isPortableIdRef)(value)) {
|
|
289
|
+
throw formatError(filePath, `${key}[${index}] must be a lowercase portable id or qid`);
|
|
285
290
|
}
|
|
286
291
|
}
|
|
287
292
|
}
|
|
@@ -361,7 +366,7 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
|
|
|
361
366
|
}
|
|
362
367
|
case "work": {
|
|
363
368
|
const agentId = expectString(frontmatter, "agent_id", filePath);
|
|
364
|
-
|
|
369
|
+
requirePortableIdRef(agentId, "agent_id", filePath);
|
|
365
370
|
const kind = expectString(frontmatter, "kind", filePath);
|
|
366
371
|
requireLowerToken(kind, "kind", filePath);
|
|
367
372
|
const pricingModel = expectString(frontmatter, "pricing_model", filePath);
|
|
@@ -380,7 +385,7 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
|
|
|
380
385
|
}
|
|
381
386
|
case "work_order": {
|
|
382
387
|
const workId = expectString(frontmatter, "work_id", filePath);
|
|
383
|
-
|
|
388
|
+
requirePortableIdRef(workId, "work_id", filePath);
|
|
384
389
|
const workVersion = expectString(frontmatter, "work_version", filePath);
|
|
385
390
|
requireSemver(workVersion, "work_version", filePath);
|
|
386
391
|
const requester = expectRefString(frontmatter, "requester", filePath);
|
|
@@ -402,7 +407,7 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
|
|
|
402
407
|
}
|
|
403
408
|
case "receipt": {
|
|
404
409
|
const workOrderId = expectString(frontmatter, "work_order_id", filePath);
|
|
405
|
-
|
|
410
|
+
requirePortableIdRef(workOrderId, "work_order_id", filePath);
|
|
406
411
|
const receiptStatus = expectString(frontmatter, "receipt_status", filePath);
|
|
407
412
|
requireEnum(receiptStatus, "receipt_status", RECEIPT_STATUS_VALUES, filePath);
|
|
408
413
|
const outcome = expectString(frontmatter, "outcome", filePath);
|
|
@@ -419,7 +424,7 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
|
|
|
419
424
|
}
|
|
420
425
|
case "feedback": {
|
|
421
426
|
const targetId = expectString(frontmatter, "target_id", filePath);
|
|
422
|
-
|
|
427
|
+
requirePortableIdRef(targetId, "target_id", filePath);
|
|
423
428
|
const sentiment = expectString(frontmatter, "sentiment", filePath);
|
|
424
429
|
requireEnum(sentiment, "sentiment", SENTIMENT_VALUES, filePath);
|
|
425
430
|
const feedbackStatus = expectString(frontmatter, "feedback_status", filePath);
|
|
@@ -429,9 +434,9 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
|
|
|
429
434
|
}
|
|
430
435
|
case "dispute": {
|
|
431
436
|
const workOrderId = expectString(frontmatter, "work_order_id", filePath);
|
|
432
|
-
|
|
437
|
+
requirePortableIdRef(workOrderId, "work_order_id", filePath);
|
|
433
438
|
const receiptId = expectString(frontmatter, "receipt_id", filePath);
|
|
434
|
-
|
|
439
|
+
requirePortableIdRef(receiptId, "receipt_id", filePath);
|
|
435
440
|
const disputeStatus = expectString(frontmatter, "dispute_status", filePath);
|
|
436
441
|
requireEnum(disputeStatus, "dispute_status", DISPUTE_STATUS_VALUES, filePath);
|
|
437
442
|
const severity = expectString(frontmatter, "severity", filePath);
|
|
@@ -440,7 +445,7 @@ function validateAgentFrontmatter(type, frontmatter, filePath) {
|
|
|
440
445
|
}
|
|
441
446
|
case "proposal": {
|
|
442
447
|
const targetId = expectString(frontmatter, "target_id", filePath);
|
|
443
|
-
|
|
448
|
+
requirePortableIdRef(targetId, "target_id", filePath);
|
|
444
449
|
const proposalStatus = expectString(frontmatter, "proposal_status", filePath);
|
|
445
450
|
requireEnum(proposalStatus, "proposal_status", PROPOSAL_STATUS_VALUES, filePath);
|
|
446
451
|
const proposalKind = expectString(frontmatter, "proposal_kind", filePath);
|
package/dist/graph/node.js
CHANGED
|
@@ -308,7 +308,7 @@ function parseNode(content, filePath, options) {
|
|
|
308
308
|
const owners = requireLowercaseList(optionalList(frontmatter, "owners", filePath), "owners", filePath);
|
|
309
309
|
const links = optionalList(frontmatter, "links", filePath);
|
|
310
310
|
const artifacts = optionalList(frontmatter, "artifacts", filePath);
|
|
311
|
-
const refs = normalizeRefsList(optionalList(frontmatter, "refs", filePath), "refs", filePath,
|
|
311
|
+
const refs = normalizeRefsList(optionalList(frontmatter, "refs", filePath), "refs", filePath, true);
|
|
312
312
|
const aliases = requireLowercaseList(optionalList(frontmatter, "aliases", filePath), "aliases", filePath);
|
|
313
313
|
const skillsRaw = optionalList(frontmatter, "skills", filePath);
|
|
314
314
|
const skills = normalizeSkillList(skillsRaw, filePath);
|
|
@@ -326,7 +326,7 @@ function parseNode(content, filePath, options) {
|
|
|
326
326
|
throw formatError(filePath, "supersedes must be a dec-# id");
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
|
-
const edges = (0, edges_1.extractEdges)(frontmatter, filePath, { allowPortableRefs:
|
|
329
|
+
const edges = (0, edges_1.extractEdges)(frontmatter, filePath, { allowPortableRefs: true });
|
|
330
330
|
const attributes = {
|
|
331
331
|
...extractGoalAttributes(type, frontmatter),
|
|
332
332
|
...(0, agent_file_types_1.extractAgentAttributes)(type, frontmatter),
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isSkillsIndexStale = isSkillsIndexStale;
|
|
6
7
|
exports.writeSkillsIndex = writeSkillsIndex;
|
|
7
8
|
exports.loadSkillsIndex = loadSkillsIndex;
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.SQLITE_SCHEMA_VERSION = void 0;
|
|
7
7
|
exports.isSqliteBackend = isSqliteBackend;
|
|
8
8
|
exports.resolveSqlitePath = resolveSqlitePath;
|
|
9
|
+
exports.sqliteSourceFingerprint = sqliteSourceFingerprint;
|
|
10
|
+
exports.readSqliteIndexMeta = readSqliteIndexMeta;
|
|
9
11
|
exports.writeSqliteIndex = writeSqliteIndex;
|
|
10
12
|
exports.reserveSqliteNumericId = reserveSqliteNumericId;
|
|
11
13
|
exports.sqliteHealth = sqliteHealth;
|
|
@@ -153,6 +155,29 @@ function buildSourceFingerprint(options) {
|
|
|
153
155
|
};
|
|
154
156
|
return `sha256:${crypto_1.default.createHash("sha256").update(stableCacheJson(payload)).digest("hex")}`;
|
|
155
157
|
}
|
|
158
|
+
function sqliteSourceFingerprint(options) {
|
|
159
|
+
const nodeHashes = new Map();
|
|
160
|
+
for (const node of Object.values(options.nodeIndex.nodes)) {
|
|
161
|
+
nodeHashes.set(node.qid, nodeSourceHash(options.root, node.path));
|
|
162
|
+
}
|
|
163
|
+
return buildSourceFingerprint({ ...options, nodeHashes });
|
|
164
|
+
}
|
|
165
|
+
function readSqliteIndexMeta(root, config) {
|
|
166
|
+
const sqlitePath = resolveSqlitePath(root, config);
|
|
167
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
168
|
+
const db = new DatabaseSync(sqlitePath);
|
|
169
|
+
try {
|
|
170
|
+
const rows = db.prepare("SELECT key, value FROM meta").all();
|
|
171
|
+
const meta = {};
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
meta[String(row.key)] = String(row.value);
|
|
174
|
+
}
|
|
175
|
+
return meta;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
db.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
156
181
|
function writeSqliteIndex(options) {
|
|
157
182
|
const sqlitePath = resolveSqlitePath(options.root, options.config);
|
|
158
183
|
fs_1.default.mkdirSync(path_1.default.dirname(sqlitePath), { recursive: true });
|