mdkg 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -15
- package/CLI_COMMAND_MATRIX.md +109 -2
- package/README.md +40 -2
- package/dist/cli.js +262 -2
- package/dist/command-contract.json +1902 -524
- package/dist/commands/archive.js +31 -14
- package/dist/commands/bundle.js +180 -44
- package/dist/commands/capability.js +1 -0
- package/dist/commands/checkpoint.js +6 -5
- package/dist/commands/event_support.js +16 -7
- package/dist/commands/fix.js +11 -8
- package/dist/commands/git.js +624 -0
- package/dist/commands/goal.js +23 -23
- package/dist/commands/graph.js +64 -10
- package/dist/commands/handoff.js +4 -2
- package/dist/commands/init.js +28 -23
- package/dist/commands/loop.js +1668 -0
- package/dist/commands/loop_descriptors.js +219 -0
- package/dist/commands/mcp.js +101 -11
- package/dist/commands/new.js +49 -3
- package/dist/commands/pack.js +14 -7
- package/dist/commands/search.js +10 -1
- package/dist/commands/skill.js +12 -9
- package/dist/commands/skill_mirror.js +39 -31
- package/dist/commands/subgraph.js +59 -26
- package/dist/commands/task.js +77 -4
- package/dist/commands/upgrade.js +13 -15
- package/dist/commands/validate.js +20 -7
- package/dist/commands/work.js +44 -26
- package/dist/commands/workspace.js +10 -4
- package/dist/core/config.js +45 -17
- package/dist/core/filesystem_authority.js +281 -0
- package/dist/core/project_db_events.js +4 -0
- package/dist/core/project_db_materializer.js +2 -0
- package/dist/core/project_db_migrations.js +238 -181
- package/dist/core/project_db_snapshot.js +64 -14
- package/dist/core/workspace_path.js +7 -0
- package/dist/graph/archive_integrity.js +1 -1
- package/dist/graph/capabilities_index_cache.js +4 -1
- package/dist/graph/frontmatter.js +38 -0
- package/dist/graph/goal_scope.js +4 -1
- package/dist/graph/index_cache.js +37 -7
- package/dist/graph/loop_bindings.js +39 -0
- package/dist/graph/node.js +209 -1
- package/dist/graph/node_body.js +52 -6
- package/dist/graph/skills_index_cache.js +41 -6
- package/dist/graph/skills_indexer.js +5 -2
- package/dist/graph/sqlite_index.js +118 -105
- package/dist/graph/subgraphs.js +142 -5
- package/dist/graph/validate_graph.js +109 -13
- package/dist/graph/workspace_files.js +39 -9
- package/dist/init/AGENT_START.md +16 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
- package/dist/init/config.json +7 -1
- package/dist/init/init-manifest.json +49 -4
- package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
- package/dist/init/templates/default/loop.md +160 -0
- package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
- package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
- package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
- package/dist/init/templates/loops/security-audit.loop.md +140 -0
- package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
- package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
- package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
- package/dist/pack/order.js +3 -1
- package/dist/pack/pack.js +139 -6
- package/dist/templates/loader.js +9 -3
- package/dist/util/argparse.js +5 -0
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
package/dist/graph/node_body.js
CHANGED
|
@@ -3,13 +3,52 @@ 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.DEFAULT_NODE_BODY_MAX_BYTES = void 0;
|
|
7
|
+
exports.createNodeBodyReader = createNodeBodyReader;
|
|
6
8
|
exports.readNodeBody = readNodeBody;
|
|
7
9
|
const fs_1 = __importDefault(require("fs"));
|
|
8
10
|
const path_1 = __importDefault(require("path"));
|
|
9
11
|
const frontmatter_1 = require("./frontmatter");
|
|
10
12
|
const zip_1 = require("../util/zip");
|
|
11
13
|
const errors_1 = require("../util/errors");
|
|
12
|
-
|
|
14
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
15
|
+
exports.DEFAULT_NODE_BODY_MAX_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
function createNodeBodyReader(root, maxBytes = exports.DEFAULT_NODE_BODY_MAX_BYTES) {
|
|
17
|
+
const bundleEntries = new Map();
|
|
18
|
+
return (node) => {
|
|
19
|
+
if (!node.source?.imported) {
|
|
20
|
+
const filePath = path_1.default.resolve(root, node.path);
|
|
21
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
22
|
+
throw new errors_1.NotFoundError(`file not found for ${node.qid}: ${node.path}`);
|
|
23
|
+
}
|
|
24
|
+
const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: node.path, maxBytes }, "utf8");
|
|
25
|
+
const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
26
|
+
if (parsed.frontmatter.id !== node.id || parsed.frontmatter.type !== node.type) {
|
|
27
|
+
throw new Error(`cached node identity mismatch for ${node.qid}`);
|
|
28
|
+
}
|
|
29
|
+
return parsed.body.trimEnd();
|
|
30
|
+
}
|
|
31
|
+
const source = node.source;
|
|
32
|
+
const bundlePath = path_1.default.resolve(root, source.bundle_path);
|
|
33
|
+
let entries = bundleEntries.get(bundlePath);
|
|
34
|
+
if (!entries) {
|
|
35
|
+
if (!fs_1.default.existsSync(bundlePath)) {
|
|
36
|
+
throw new errors_1.NotFoundError(`bundle not found for ${node.qid}: ${source.bundle_path}`);
|
|
37
|
+
}
|
|
38
|
+
entries = new Map((0, zip_1.readZipFileEntries)(bundlePath).map((entry) => [entry.name, entry.data]));
|
|
39
|
+
bundleEntries.set(bundlePath, entries);
|
|
40
|
+
}
|
|
41
|
+
const entry = entries.get(source.original_path);
|
|
42
|
+
if (!entry) {
|
|
43
|
+
throw new errors_1.NotFoundError(`bundle entry not found for ${node.qid}: ${source.original_path}`);
|
|
44
|
+
}
|
|
45
|
+
if (entry.length > maxBytes) {
|
|
46
|
+
throw new Error(`node body source exceeds byte limit for ${node.qid}: ${maxBytes}`);
|
|
47
|
+
}
|
|
48
|
+
return (0, frontmatter_1.parseFrontmatter)(entry.toString("utf8"), source.original_path).body.trimEnd();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function readImportedBody(root, node, maxBytes) {
|
|
13
52
|
const source = node.source;
|
|
14
53
|
if (!source?.imported) {
|
|
15
54
|
throw new Error("node is not imported");
|
|
@@ -18,20 +57,27 @@ function readImportedBody(root, node) {
|
|
|
18
57
|
if (!fs_1.default.existsSync(bundlePath)) {
|
|
19
58
|
throw new errors_1.NotFoundError(`bundle not found for ${node.qid}: ${source.bundle_path}`);
|
|
20
59
|
}
|
|
21
|
-
const entry = (0, zip_1.
|
|
60
|
+
const entry = (0, zip_1.readZipFileEntries)(bundlePath).find((candidate) => candidate.name === source.original_path);
|
|
22
61
|
if (!entry) {
|
|
23
62
|
throw new errors_1.NotFoundError(`bundle entry not found for ${node.qid}: ${source.original_path}`);
|
|
24
63
|
}
|
|
64
|
+
if (entry.data.length > maxBytes) {
|
|
65
|
+
throw new Error(`node body source exceeds byte limit for ${node.qid}: ${maxBytes}`);
|
|
66
|
+
}
|
|
25
67
|
return (0, frontmatter_1.parseFrontmatter)(entry.data.toString("utf8"), source.original_path).body.trimEnd();
|
|
26
68
|
}
|
|
27
|
-
function readNodeBody(root, node) {
|
|
69
|
+
function readNodeBody(root, node, maxBytes = exports.DEFAULT_NODE_BODY_MAX_BYTES) {
|
|
28
70
|
if (node.source?.imported) {
|
|
29
|
-
return readImportedBody(root, node);
|
|
71
|
+
return readImportedBody(root, node, maxBytes);
|
|
30
72
|
}
|
|
31
73
|
const filePath = path_1.default.resolve(root, node.path);
|
|
32
74
|
if (!fs_1.default.existsSync(filePath)) {
|
|
33
75
|
throw new errors_1.NotFoundError(`file not found for ${node.qid}: ${node.path}`);
|
|
34
76
|
}
|
|
35
|
-
const content =
|
|
36
|
-
|
|
77
|
+
const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: node.path, maxBytes }, "utf8");
|
|
78
|
+
const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
79
|
+
if (parsed.frontmatter.id !== node.id || parsed.frontmatter.type !== node.type) {
|
|
80
|
+
throw new Error(`cached node identity mismatch for ${node.qid}`);
|
|
81
|
+
}
|
|
82
|
+
return parsed.body.trimEnd();
|
|
37
83
|
}
|
|
@@ -10,6 +10,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const paths_1 = require("../core/paths");
|
|
12
12
|
const atomic_1 = require("../util/atomic");
|
|
13
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
13
14
|
const skills_indexer_1 = require("./skills_indexer");
|
|
14
15
|
function mtimeMs(filePath) {
|
|
15
16
|
return fs_1.default.statSync(filePath).mtimeMs;
|
|
@@ -50,16 +51,39 @@ function isSkillsIndexStale(root, config) {
|
|
|
50
51
|
}
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
|
-
function
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function readSkillsIndex(root, indexPath) {
|
|
54
58
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
59
|
+
const relativePath = path_1.default.relative(root, indexPath).split(path_1.default.sep).join("/");
|
|
60
|
+
const parsed = JSON.parse((0, filesystem_authority_1.readContainedFile)({ root, relativePath }, "utf8"));
|
|
61
|
+
if (!isRecord(parsed) || !isRecord(parsed.meta) || !isRecord(parsed.skills)) {
|
|
62
|
+
throw new Error("skills index cache has an invalid shape");
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
57
65
|
}
|
|
58
66
|
catch (err) {
|
|
59
67
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
60
68
|
throw new Error(`failed to read skills index: ${message}`);
|
|
61
69
|
}
|
|
62
70
|
}
|
|
71
|
+
function validateCachedSkillPaths(root, config, cached) {
|
|
72
|
+
const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
|
|
73
|
+
for (const [slug, skill] of Object.entries(cached.skills)) {
|
|
74
|
+
if (skill.slug !== slug || skill.id !== `skill:${slug}` || skill.qid !== `${skill.ws}:skill:${slug}`) {
|
|
75
|
+
throw new Error(`invalid cached skill identity: ${slug}`);
|
|
76
|
+
}
|
|
77
|
+
const absolutePath = path_1.default.resolve(root, skill.path);
|
|
78
|
+
const relativeSkillsPath = path_1.default.relative(skillsRoot, absolutePath);
|
|
79
|
+
if (path_1.default.isAbsolute(skill.path) || relativeSkillsPath.startsWith("..") || path_1.default.isAbsolute(relativeSkillsPath)) {
|
|
80
|
+
throw new Error(`cached skill path escapes skills root: ${slug}`);
|
|
81
|
+
}
|
|
82
|
+
const normalized = path_1.default.relative(root, absolutePath).split(path_1.default.sep).join("/");
|
|
83
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: normalized, operation: "read" }, () => undefined);
|
|
84
|
+
}
|
|
85
|
+
return cached;
|
|
86
|
+
}
|
|
63
87
|
function writeSkillsIndex(indexPath, index) {
|
|
64
88
|
const sortedSkills = {};
|
|
65
89
|
for (const slug of Object.keys(index.skills).sort()) {
|
|
@@ -74,6 +98,7 @@ function writeSkillsIndex(indexPath, index) {
|
|
|
74
98
|
function loadSkillsIndex(options) {
|
|
75
99
|
const useCache = options.useCache ?? true;
|
|
76
100
|
const allowReindex = options.allowReindex ?? options.config.index.auto_reindex;
|
|
101
|
+
const persistReindex = options.persistReindex ?? true;
|
|
77
102
|
const indexPath = (0, skills_indexer_1.resolveSkillsIndexPath)(options.root);
|
|
78
103
|
if (!useCache) {
|
|
79
104
|
const index = (0, skills_indexer_1.buildSkillsIndex)(options.root, options.config);
|
|
@@ -81,15 +106,25 @@ function loadSkillsIndex(options) {
|
|
|
81
106
|
}
|
|
82
107
|
const stale = isSkillsIndexStale(options.root, options.config);
|
|
83
108
|
if (fs_1.default.existsSync(indexPath) && !stale) {
|
|
84
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
index: validateCachedSkillPaths(options.root, options.config, readSkillsIndex(options.root, indexPath)),
|
|
111
|
+
rebuilt: false,
|
|
112
|
+
stale: false,
|
|
113
|
+
};
|
|
85
114
|
}
|
|
86
115
|
if (allowReindex) {
|
|
87
116
|
const index = (0, skills_indexer_1.buildSkillsIndex)(options.root, options.config);
|
|
88
|
-
|
|
117
|
+
if (persistReindex) {
|
|
118
|
+
writeSkillsIndex(indexPath, index);
|
|
119
|
+
}
|
|
89
120
|
return { index, rebuilt: true, stale };
|
|
90
121
|
}
|
|
91
122
|
if (fs_1.default.existsSync(indexPath)) {
|
|
92
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
index: validateCachedSkillPaths(options.root, options.config, readSkillsIndex(options.root, indexPath)),
|
|
125
|
+
rebuilt: false,
|
|
126
|
+
stale: true,
|
|
127
|
+
};
|
|
93
128
|
}
|
|
94
129
|
throw new Error("skills index missing and auto-reindex is disabled");
|
|
95
130
|
}
|
|
@@ -12,6 +12,7 @@ exports.buildSkillIndexEntry = buildSkillIndexEntry;
|
|
|
12
12
|
exports.buildSkillsIndex = buildSkillsIndex;
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
15
16
|
const frontmatter_1 = require("./frontmatter");
|
|
16
17
|
exports.SKILLS_INDEX_RELATIVE_PATH = ".mdkg/index/skills.json";
|
|
17
18
|
exports.SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -94,7 +95,8 @@ function hasDirectory(dirPath) {
|
|
|
94
95
|
if (!fs_1.default.existsSync(dirPath)) {
|
|
95
96
|
return false;
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
+
const stat = fs_1.default.lstatSync(dirPath);
|
|
99
|
+
return !stat.isSymbolicLink() && stat.isDirectory();
|
|
98
100
|
}
|
|
99
101
|
function rootWorkspaceMdkgPath(root, config) {
|
|
100
102
|
const rootWs = config.workspaces.root;
|
|
@@ -113,7 +115,8 @@ function buildSkillIndexEntryForWorkspace(root, workspace, slug, filePath) {
|
|
|
113
115
|
if (!exports.SKILL_SLUG_RE.test(slug)) {
|
|
114
116
|
throw new Error(`${filePath}: skill slug must be kebab-case`);
|
|
115
117
|
}
|
|
116
|
-
const
|
|
118
|
+
const relativePath = path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
|
|
119
|
+
const content = (0, filesystem_authority_1.readContainedFile)({ root, relativePath });
|
|
117
120
|
const { frontmatter } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
118
121
|
const name = requireString(frontmatter, "name", filePath);
|
|
119
122
|
const description = requireString(frontmatter, "description", filePath);
|
|
@@ -14,6 +14,7 @@ exports.sqliteHealth = sqliteHealth;
|
|
|
14
14
|
const crypto_1 = __importDefault(require("crypto"));
|
|
15
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
17
18
|
const staleness_1 = require("./staleness");
|
|
18
19
|
exports.SQLITE_SCHEMA_VERSION = 2;
|
|
19
20
|
function loadDatabaseCtor() {
|
|
@@ -61,8 +62,9 @@ function isSqliteBackend(config) {
|
|
|
61
62
|
function resolveSqlitePath(root, config) {
|
|
62
63
|
return path_1.default.resolve(root, config.index.sqlite_path);
|
|
63
64
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
65
|
+
function sqliteTempRelativePath(sqliteRelativePath) {
|
|
66
|
+
const normalized = sqliteRelativePath.replace(/\\/g, "/");
|
|
67
|
+
return path_1.default.posix.join(path_1.default.posix.dirname(normalized), `.${path_1.default.posix.basename(normalized)}.${process.pid}.${Date.now()}.tmp`);
|
|
66
68
|
}
|
|
67
69
|
function createSchema(db) {
|
|
68
70
|
db.exec(`
|
|
@@ -163,127 +165,136 @@ function sqliteSourceFingerprint(options) {
|
|
|
163
165
|
return buildSourceFingerprint({ ...options, nodeHashes });
|
|
164
166
|
}
|
|
165
167
|
function readSqliteIndexMeta(root, config) {
|
|
166
|
-
const sqlitePath = resolveSqlitePath(root, config);
|
|
167
168
|
const DatabaseSync = loadDatabaseCtor();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
return (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: config.index.sqlite_path, operation: "read" }, ({ absolutePath }) => {
|
|
170
|
+
const db = new DatabaseSync(absolutePath);
|
|
171
|
+
try {
|
|
172
|
+
const rows = db.prepare("SELECT key, value FROM meta").all();
|
|
173
|
+
const meta = {};
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
meta[String(row.key)] = String(row.value);
|
|
176
|
+
}
|
|
177
|
+
return meta;
|
|
174
178
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
179
|
+
finally {
|
|
180
|
+
db.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
180
183
|
}
|
|
181
184
|
function writeSqliteIndex(options) {
|
|
185
|
+
const sqliteRelativePath = options.config.index.sqlite_path;
|
|
182
186
|
const sqlitePath = resolveSqlitePath(options.root, options.config);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
fs_1.default.rmSync(tempPath, { force: true });
|
|
187
|
+
const tempRelativePath = sqliteTempRelativePath(sqliteRelativePath);
|
|
188
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: sqliteRelativePath, operation: "replace", createParents: true }, () => undefined);
|
|
186
189
|
const DatabaseSync = loadDatabaseCtor();
|
|
187
|
-
const db = new DatabaseSync(tempPath);
|
|
188
190
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
insertMeta(db, "schema_version", String(exports.SQLITE_SCHEMA_VERSION));
|
|
196
|
-
insertMeta(db, "package_schema_version", String(options.config.schema_version));
|
|
197
|
-
insertMeta(db, "backend", options.config.index.backend);
|
|
198
|
-
insertMeta(db, "source_fingerprint", buildSourceFingerprint({ ...options, nodeHashes }));
|
|
199
|
-
insertMeta(db, "root", ".");
|
|
200
|
-
const insertNode = db.prepare("INSERT INTO nodes (qid, id, ws, type, title, path, status, priority, updated, source_hash, json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
201
|
-
const insertEdge = db.prepare("INSERT OR IGNORE INTO edges (source_qid, kind, target_qid) VALUES (?, ?, ?)");
|
|
202
|
-
for (const node of Object.values(options.nodeIndex.nodes).sort((a, b) => a.qid.localeCompare(b.qid))) {
|
|
203
|
-
insertNode.run(node.qid, node.id, node.ws, node.type, node.title, toPosixPath(node.path), node.status ?? null, node.priority ?? null, node.updated ?? null, nodeHashes.get(node.qid) ?? null, stableCacheJson(node));
|
|
204
|
-
for (const [kind, values] of [
|
|
205
|
-
["relates", node.edges.relates],
|
|
206
|
-
["blocked_by", node.edges.blocked_by],
|
|
207
|
-
["blocks", node.edges.blocks],
|
|
208
|
-
["context_refs", node.edges.context_refs ?? []],
|
|
209
|
-
["evidence_refs", node.edges.evidence_refs ?? []],
|
|
210
|
-
]) {
|
|
211
|
-
for (const target of values) {
|
|
212
|
-
insertEdge.run(node.qid, kind, target);
|
|
191
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: tempRelativePath, operation: "create", createParents: true }, ({ absolutePath: tempPath }) => {
|
|
192
|
+
const db = new DatabaseSync(tempPath);
|
|
193
|
+
try {
|
|
194
|
+
const nodeHashes = new Map();
|
|
195
|
+
for (const node of Object.values(options.nodeIndex.nodes)) {
|
|
196
|
+
nodeHashes.set(node.qid, nodeSourceHash(options.root, node.path));
|
|
213
197
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
198
|
+
createSchema(db);
|
|
199
|
+
insertMeta(db, "tool", "mdkg");
|
|
200
|
+
insertMeta(db, "schema_version", String(exports.SQLITE_SCHEMA_VERSION));
|
|
201
|
+
insertMeta(db, "package_schema_version", String(options.config.schema_version));
|
|
202
|
+
insertMeta(db, "backend", options.config.index.backend);
|
|
203
|
+
insertMeta(db, "source_fingerprint", buildSourceFingerprint({ ...options, nodeHashes }));
|
|
204
|
+
insertMeta(db, "root", ".");
|
|
205
|
+
const insertNode = db.prepare("INSERT INTO nodes (qid, id, ws, type, title, path, status, priority, updated, source_hash, json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
206
|
+
const insertEdge = db.prepare("INSERT OR IGNORE INTO edges (source_qid, kind, target_qid) VALUES (?, ?, ?)");
|
|
207
|
+
for (const node of Object.values(options.nodeIndex.nodes).sort((a, b) => a.qid.localeCompare(b.qid))) {
|
|
208
|
+
insertNode.run(node.qid, node.id, node.ws, node.type, node.title, toPosixPath(node.path), node.status ?? null, node.priority ?? null, node.updated ?? null, nodeHashes.get(node.qid) ?? null, stableCacheJson(node));
|
|
209
|
+
for (const [kind, values] of [
|
|
210
|
+
["relates", node.edges.relates],
|
|
211
|
+
["blocked_by", node.edges.blocked_by],
|
|
212
|
+
["blocks", node.edges.blocks],
|
|
213
|
+
["context_refs", node.edges.context_refs ?? []],
|
|
214
|
+
["evidence_refs", node.edges.evidence_refs ?? []],
|
|
215
|
+
]) {
|
|
216
|
+
for (const target of values) {
|
|
217
|
+
insertEdge.run(node.qid, kind, target);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const [kind, target] of [
|
|
221
|
+
["epic", node.edges.epic],
|
|
222
|
+
["parent", node.edges.parent],
|
|
223
|
+
["prev", node.edges.prev],
|
|
224
|
+
["next", node.edges.next],
|
|
225
|
+
]) {
|
|
226
|
+
if (target) {
|
|
227
|
+
insertEdge.run(node.qid, kind, target);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const insertSkill = db.prepare("INSERT INTO skills (qid, slug, ws, name, path, json) VALUES (?, ?, ?, ?, ?, ?)");
|
|
232
|
+
for (const skill of Object.values(options.skillsIndex.skills).sort((a, b) => a.qid.localeCompare(b.qid))) {
|
|
233
|
+
insertSkill.run(skill.qid, skill.slug, skill.ws, skill.name, skill.path, stableCacheJson(skill));
|
|
234
|
+
}
|
|
235
|
+
const insertCapability = db.prepare("INSERT INTO capabilities (qid, kind, workspace, visibility, id, path, source_hash, json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
236
|
+
for (const record of options.capabilitiesIndex.records) {
|
|
237
|
+
insertCapability.run(record.qid, record.kind, record.workspace, record.visibility, record.id, record.path, record.source_hash, stableCacheJson(record));
|
|
238
|
+
}
|
|
239
|
+
const insertArchive = db.prepare("INSERT INTO archives (qid, visibility, compressed_path, compressed_sha256, json) VALUES (?, ?, ?, ?, ?)");
|
|
240
|
+
for (const node of Object.values(options.nodeIndex.nodes).filter((item) => item.type === "archive")) {
|
|
241
|
+
insertArchive.run(node.qid, String(node.attributes.visibility ?? "private"), String(node.attributes.compressed_path ?? ""), String(node.attributes.compressed_sha256 ?? ""), stableCacheJson(node));
|
|
242
|
+
}
|
|
243
|
+
const insertSubgraph = db.prepare("INSERT INTO subgraphs (alias, enabled, stale, error_count, warning_count, json) VALUES (?, ?, ?, ?, ?, ?)");
|
|
244
|
+
for (const item of options.subgraphsIndex.subgraphs) {
|
|
245
|
+
insertSubgraph.run(item.alias, item.enabled ? 1 : 0, item.stale ? 1 : 0, item.error_count, item.warning_count, stableCacheJson(item));
|
|
223
246
|
}
|
|
224
247
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
const insertCapability = db.prepare("INSERT INTO capabilities (qid, kind, workspace, visibility, id, path, source_hash, json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
231
|
-
for (const record of options.capabilitiesIndex.records) {
|
|
232
|
-
insertCapability.run(record.qid, record.kind, record.workspace, record.visibility, record.id, record.path, record.source_hash, stableCacheJson(record));
|
|
233
|
-
}
|
|
234
|
-
const insertArchive = db.prepare("INSERT INTO archives (qid, visibility, compressed_path, compressed_sha256, json) VALUES (?, ?, ?, ?, ?)");
|
|
235
|
-
for (const node of Object.values(options.nodeIndex.nodes).filter((item) => item.type === "archive")) {
|
|
236
|
-
insertArchive.run(node.qid, String(node.attributes.visibility ?? "private"), String(node.attributes.compressed_path ?? ""), String(node.attributes.compressed_sha256 ?? ""), stableCacheJson(node));
|
|
237
|
-
}
|
|
238
|
-
const insertSubgraph = db.prepare("INSERT INTO subgraphs (alias, enabled, stale, error_count, warning_count, json) VALUES (?, ?, ?, ?, ?, ?)");
|
|
239
|
-
for (const item of options.subgraphsIndex.subgraphs) {
|
|
240
|
-
insertSubgraph.run(item.alias, item.enabled ? 1 : 0, item.stale ? 1 : 0, item.error_count, item.warning_count, stableCacheJson(item));
|
|
241
|
-
}
|
|
248
|
+
finally {
|
|
249
|
+
db.close();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: sqliteRelativePath, operation: "replace", createParents: true }, ({ absolutePath: targetPath }) => (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: tempRelativePath, operation: "read" }, ({ absolutePath: tempPath }) => fs_1.default.renameSync(tempPath, targetPath)));
|
|
242
253
|
}
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
catch (error) {
|
|
255
|
+
(0, filesystem_authority_1.removeContainedPath)({ root: options.root, relativePath: tempRelativePath, force: true });
|
|
256
|
+
throw error;
|
|
245
257
|
}
|
|
246
|
-
fs_1.default.renameSync(tempPath, sqlitePath);
|
|
247
258
|
return sqlitePath;
|
|
248
259
|
}
|
|
249
260
|
function reserveSqliteNumericId(options) {
|
|
250
261
|
if (!isSqliteBackend(options.config)) {
|
|
251
262
|
return undefined;
|
|
252
263
|
}
|
|
253
|
-
const sqlitePath = resolveSqlitePath(options.root, options.config);
|
|
254
|
-
fs_1.default.mkdirSync(path_1.default.dirname(sqlitePath), { recursive: true });
|
|
255
264
|
const DatabaseSync = loadDatabaseCtor();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
db.exec("CREATE TABLE IF NOT EXISTS id_allocations (ws TEXT NOT NULL, prefix TEXT NOT NULL, next_value INTEGER NOT NULL, PRIMARY KEY (ws, prefix));");
|
|
259
|
-
db.exec("BEGIN IMMEDIATE");
|
|
260
|
-
const row = db
|
|
261
|
-
.prepare("SELECT next_value FROM id_allocations WHERE ws = ? AND prefix = ?")
|
|
262
|
-
.get(options.ws, options.prefix);
|
|
263
|
-
const existing = typeof row?.next_value === "number" ? row.next_value : undefined;
|
|
264
|
-
const nextValue = Math.max(existing ?? 1, options.currentMax + 1);
|
|
265
|
-
db.prepare("INSERT INTO id_allocations (ws, prefix, next_value) VALUES (?, ?, ?) ON CONFLICT(ws, prefix) DO UPDATE SET next_value = excluded.next_value").run(options.ws, options.prefix, nextValue + 1);
|
|
266
|
-
db.exec("COMMIT");
|
|
267
|
-
return `${options.prefix}-${nextValue}`;
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
265
|
+
return (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath: options.config.index.sqlite_path, operation: "replace", createParents: true }, ({ absolutePath }) => {
|
|
266
|
+
const db = new DatabaseSync(absolutePath);
|
|
270
267
|
try {
|
|
271
|
-
db.exec("
|
|
268
|
+
db.exec("CREATE TABLE IF NOT EXISTS id_allocations (ws TEXT NOT NULL, prefix TEXT NOT NULL, next_value INTEGER NOT NULL, PRIMARY KEY (ws, prefix));");
|
|
269
|
+
db.exec("BEGIN IMMEDIATE");
|
|
270
|
+
const row = db
|
|
271
|
+
.prepare("SELECT next_value FROM id_allocations WHERE ws = ? AND prefix = ?")
|
|
272
|
+
.get(options.ws, options.prefix);
|
|
273
|
+
const existing = typeof row?.next_value === "number" ? row.next_value : undefined;
|
|
274
|
+
const nextValue = Math.max(existing ?? 1, options.currentMax + 1);
|
|
275
|
+
db.prepare("INSERT INTO id_allocations (ws, prefix, next_value) VALUES (?, ?, ?) ON CONFLICT(ws, prefix) DO UPDATE SET next_value = excluded.next_value").run(options.ws, options.prefix, nextValue + 1);
|
|
276
|
+
db.exec("COMMIT");
|
|
277
|
+
return `${options.prefix}-${nextValue}`;
|
|
272
278
|
}
|
|
273
|
-
catch {
|
|
274
|
-
|
|
279
|
+
catch (err) {
|
|
280
|
+
try {
|
|
281
|
+
db.exec("ROLLBACK");
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// ignore rollback failures when no transaction is active
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
275
287
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
288
|
+
finally {
|
|
289
|
+
db.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
281
292
|
}
|
|
282
293
|
function sqliteHealth(root, config) {
|
|
283
294
|
const sqlitePath = resolveSqlitePath(root, config);
|
|
284
295
|
const warnings = [];
|
|
285
296
|
const errors = [];
|
|
286
|
-
if (!
|
|
297
|
+
if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: config.index.sqlite_path })) {
|
|
287
298
|
warnings.push(`SQLite cache missing at ${toPosixPath(path_1.default.relative(root, sqlitePath))}; run mdkg index`);
|
|
288
299
|
return { path: sqlitePath, exists: false, size: 0, warnings, errors };
|
|
289
300
|
}
|
|
@@ -299,16 +310,18 @@ function sqliteHealth(root, config) {
|
|
|
299
310
|
}
|
|
300
311
|
try {
|
|
301
312
|
const DatabaseSync = loadDatabaseCtor();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: config.index.sqlite_path, operation: "read" }, ({ absolutePath }) => {
|
|
314
|
+
const db = new DatabaseSync(absolutePath);
|
|
315
|
+
try {
|
|
316
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
317
|
+
if (String(row?.value ?? "") !== String(exports.SQLITE_SCHEMA_VERSION)) {
|
|
318
|
+
errors.push(`SQLite schema mismatch; run mdkg index`);
|
|
319
|
+
}
|
|
307
320
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
321
|
+
finally {
|
|
322
|
+
db.close();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
312
325
|
}
|
|
313
326
|
catch (err) {
|
|
314
327
|
errors.push(`failed to read SQLite cache: ${err instanceof Error ? err.message : String(err)}`);
|