mdkg 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +88 -15
  2. package/CLI_COMMAND_MATRIX.md +74 -5
  3. package/README.md +30 -2
  4. package/dist/cli.js +132 -5
  5. package/dist/command-contract.json +778 -16
  6. package/dist/commands/archive.js +196 -42
  7. package/dist/commands/bundle.js +180 -44
  8. package/dist/commands/capability.js +1 -0
  9. package/dist/commands/checkpoint.js +6 -5
  10. package/dist/commands/event_support.js +16 -7
  11. package/dist/commands/fix.js +11 -8
  12. package/dist/commands/git.js +41 -10
  13. package/dist/commands/goal.js +23 -23
  14. package/dist/commands/graph.js +64 -10
  15. package/dist/commands/handoff.js +4 -2
  16. package/dist/commands/init.js +28 -23
  17. package/dist/commands/loop.js +1668 -0
  18. package/dist/commands/loop_descriptors.js +219 -0
  19. package/dist/commands/mcp.js +101 -11
  20. package/dist/commands/new.js +49 -3
  21. package/dist/commands/pack.js +14 -7
  22. package/dist/commands/search.js +10 -1
  23. package/dist/commands/skill.js +12 -9
  24. package/dist/commands/skill_mirror.js +39 -31
  25. package/dist/commands/subgraph.js +59 -26
  26. package/dist/commands/task.js +77 -4
  27. package/dist/commands/upgrade.js +13 -15
  28. package/dist/commands/validate.js +20 -7
  29. package/dist/commands/work.js +44 -26
  30. package/dist/commands/workspace.js +10 -4
  31. package/dist/core/config.js +45 -17
  32. package/dist/core/filesystem_authority.js +281 -0
  33. package/dist/core/project_db_events.js +4 -0
  34. package/dist/core/project_db_materializer.js +2 -0
  35. package/dist/core/project_db_migrations.js +238 -181
  36. package/dist/core/project_db_snapshot.js +64 -14
  37. package/dist/core/workspace_path.js +7 -0
  38. package/dist/graph/archive_integrity.js +1 -1
  39. package/dist/graph/capabilities_index_cache.js +4 -1
  40. package/dist/graph/frontmatter.js +38 -0
  41. package/dist/graph/goal_scope.js +4 -1
  42. package/dist/graph/index_cache.js +37 -7
  43. package/dist/graph/loop_bindings.js +39 -0
  44. package/dist/graph/node.js +209 -1
  45. package/dist/graph/node_body.js +52 -6
  46. package/dist/graph/skills_index_cache.js +41 -6
  47. package/dist/graph/skills_indexer.js +5 -2
  48. package/dist/graph/sqlite_index.js +118 -105
  49. package/dist/graph/subgraphs.js +142 -5
  50. package/dist/graph/validate_graph.js +109 -13
  51. package/dist/graph/workspace_files.js +39 -9
  52. package/dist/init/AGENT_START.md +16 -0
  53. package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
  54. package/dist/init/config.json +7 -1
  55. package/dist/init/init-manifest.json +49 -4
  56. package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
  57. package/dist/init/templates/default/loop.md +160 -0
  58. package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
  59. package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
  60. package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
  61. package/dist/init/templates/loops/security-audit.loop.md +140 -0
  62. package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
  63. package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
  64. package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
  65. package/dist/pack/order.js +3 -1
  66. package/dist/pack/pack.js +139 -6
  67. package/dist/templates/loader.js +9 -3
  68. package/dist/util/lock.js +10 -9
  69. package/dist/util/zip.js +163 -9
  70. package/package.json +8 -4
@@ -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
- function readImportedBody(root, node) {
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.readZipEntries)(fs_1.default.readFileSync(bundlePath)).find((candidate) => candidate.name === source.original_path);
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 = fs_1.default.readFileSync(filePath, "utf8");
36
- return (0, frontmatter_1.parseFrontmatter)(content, filePath).body.trimEnd();
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 readSkillsIndex(indexPath) {
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 raw = fs_1.default.readFileSync(indexPath, "utf8");
56
- return JSON.parse(raw);
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 { index: readSkillsIndex(indexPath), rebuilt: false, stale: false };
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
- writeSkillsIndex(indexPath, index);
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 { index: readSkillsIndex(indexPath), rebuilt: false, stale: true };
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
- return fs_1.default.statSync(dirPath).isDirectory();
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 content = fs_1.default.readFileSync(filePath, "utf8");
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 sqliteTempPath(sqlitePath) {
65
- return path_1.default.join(path_1.default.dirname(sqlitePath), `.${path_1.default.basename(sqlitePath)}.${process.pid}.${Date.now()}.tmp`);
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
- 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);
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
- return meta;
176
- }
177
- finally {
178
- db.close();
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
- fs_1.default.mkdirSync(path_1.default.dirname(sqlitePath), { recursive: true });
184
- const tempPath = sqliteTempPath(sqlitePath);
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
- const nodeHashes = new Map();
190
- for (const node of Object.values(options.nodeIndex.nodes)) {
191
- nodeHashes.set(node.qid, nodeSourceHash(options.root, node.path));
192
- }
193
- createSchema(db);
194
- insertMeta(db, "tool", "mdkg");
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
- for (const [kind, target] of [
216
- ["epic", node.edges.epic],
217
- ["parent", node.edges.parent],
218
- ["prev", node.edges.prev],
219
- ["next", node.edges.next],
220
- ]) {
221
- if (target) {
222
- insertEdge.run(node.qid, kind, target);
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
- const insertSkill = db.prepare("INSERT INTO skills (qid, slug, ws, name, path, json) VALUES (?, ?, ?, ?, ?, ?)");
227
- for (const skill of Object.values(options.skillsIndex.skills).sort((a, b) => a.qid.localeCompare(b.qid))) {
228
- insertSkill.run(skill.qid, skill.slug, skill.ws, skill.name, skill.path, stableCacheJson(skill));
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
- finally {
244
- db.close();
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
- const db = new DatabaseSync(sqlitePath);
257
- try {
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("ROLLBACK");
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
- // ignore rollback failures when no transaction is active
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
- throw err;
277
- }
278
- finally {
279
- db.close();
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 (!fs_1.default.existsSync(sqlitePath)) {
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
- const db = new DatabaseSync(sqlitePath);
303
- try {
304
- const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
305
- if (String(row?.value ?? "") !== String(exports.SQLITE_SCHEMA_VERSION)) {
306
- errors.push(`SQLite schema mismatch; run mdkg index`);
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
- finally {
310
- db.close();
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)}`);