mdkg 0.1.2 → 0.1.3

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 CHANGED
@@ -4,6 +4,31 @@ All notable changes to mdkg are documented here.
4
4
 
5
5
  This project follows a pragmatic changelog style inspired by Keep a Changelog. Versions use npm package versions.
6
6
 
7
+ ## 0.1.3 - Unreleased
8
+
9
+ ### Added
10
+
11
+ - Added first-class SQLite access cache support using Node's built-in `node:sqlite`; no third-party SQLite package is introduced.
12
+ - Added `.mdkg/index/mdkg.sqlite` as a rebuildable derived cache for nodes, edges, skills, capabilities, archives, bundle imports, source hashes, and schema metadata when `index.backend` is `sqlite`.
13
+ - Added fresh init defaults for `index.backend: sqlite`, `index.sqlite_path`, `index.sqlite_commit_warning_bytes`, and `index.lock_timeout_ms`.
14
+ - Added a shared mutation lock plus atomic writes for mdkg mutations and index writes.
15
+ - Added SQLite transactional id reservation for numeric node/checkpoint ids in SQLite mode.
16
+ - Added `npm run smoke:sqlite` and `npm run smoke:parallel` packed/temp-repo coverage.
17
+
18
+ ### Changed
19
+
20
+ - Raised the required Node runtime to `>=24.15.0`.
21
+ - Existing workspaces that are migrated from older configs remain on `index.backend: json` until they explicitly opt in to SQLite.
22
+ - Init ignore policy now keeps JSON indexes, temp files, lock directories, WAL, SHM, and journal files ignored while allowing `.mdkg/index/mdkg.sqlite` to be committed by intentional repo policy.
23
+ - `mdkg index` continues to write JSON compatibility indexes and also rebuilds SQLite when enabled.
24
+ - `mdkg doctor` and `mdkg validate` now report SQLite cache health when SQLite mode is enabled.
25
+ - README and seeded docs now state that mdkg is pre-v1 public alpha software and cache/DAL contracts may churn before v1.
26
+
27
+ ### Fixed
28
+
29
+ - Removed the accidental self-dependency on `mdkg` from package metadata.
30
+ - Hardened parallel `mdkg new`, checkpoint, task, work, archive, bundle import config, and index writes against naming conflicts and partial cache writes.
31
+
7
32
  ## 0.1.2 - 2026-05-19
8
33
 
9
34
  ### Added
package/README.md CHANGED
@@ -9,11 +9,14 @@ It is built for:
9
9
 
10
10
  mdkg stays deliberately boring:
11
11
  - repo-native under `.mdkg/`
12
- - TypeScript + Node.js 18+
13
- - zero runtime dependencies
14
- - no required sqlite, daemon, hosted index, or vector DB
12
+ - TypeScript + Node.js `>=24.15.0`
13
+ - zero third-party runtime dependencies
14
+ - first-class rebuildable SQLite cache through built-in `node:sqlite`
15
+ - no daemon, hosted index, or vector DB
15
16
 
16
- Current package version in source: `0.1.2`
17
+ Current package version in source: `0.1.3`
18
+
19
+ mdkg is still pre-v1 public alpha software. The public package is usable, but graph, cache, bundle, and DAL contracts may continue to change quickly while the project converges on a stable v1 surface.
17
20
 
18
21
  ## The product shape
19
22
 
@@ -202,10 +205,11 @@ mdkg lives under a hidden root directory:
202
205
  - `.mdkg/skills/` Agent Skills packages
203
206
  - `.mdkg/archive/` sidecar metadata plus deterministic compressed source/artifact caches
204
207
  - `.mdkg/bundles/` optional committed full graph snapshot bundles
208
+ - `.mdkg/index/mdkg.sqlite` optional committed, rebuildable SQLite access cache
205
209
  - `.mdkg/index/imports.json` generated read-only bundle import cache
206
210
  - `.agents/skills/` Codex/OpenAI-facing mirrored skills
207
211
  - `.claude/skills/` Claude-facing mirrored skills
208
- - `.mdkg/index/` generated cache files
212
+ - `.mdkg/index/*.json` generated JSON compatibility cache files
209
213
 
210
214
  ## Primary commands
211
215
 
@@ -292,6 +296,14 @@ The capability cache is not the full graph and is not source of truth. Normal ta
292
296
 
293
297
  Capability records aggregate enabled registered workspaces and include deterministic source metadata such as `workspace`, `visibility`, `kind`, `id`, `qid`, `path`, headings, refs, source hash, and `indexed_at`. Workspace `visibility` also feeds mdkg's export safety checks for public/internal packs and public bundles. This is a CLI safety layer, not secret scanning, body redaction, or a replacement for private git hosting.
294
298
 
299
+ ## Index backends and parallel safety
300
+
301
+ Fresh `mdkg init` workspaces default to `index.backend: sqlite`, which writes `.mdkg/index/mdkg.sqlite` as a rebuildable access cache using Node's built-in `node:sqlite`. Existing workspaces that are migrated from older configs default to `index.backend: json` until they opt in. Markdown files, archive sidecars, bundle manifests, and config remain source of truth in both modes.
302
+
303
+ `mdkg index` still writes JSON compatibility caches (`global.json`, `skills.json`, `capabilities.json`, and import projections when configured). In SQLite mode it also rebuilds the SQLite cache with nodes, edges, skills, capabilities, archive metadata, bundle imports, source hashes, and schema metadata. Deleting the SQLite file is recoverable with `mdkg index`.
304
+
305
+ Mutating commands use a workspace mutation lock plus atomic writes. SQLite mode additionally reserves numeric ids in a SQLite transaction before writing Markdown so parallel `mdkg new` and checkpoint calls avoid naming conflicts. Skipped ids after failed writes are acceptable because Markdown remains canonical.
306
+
295
307
  ## Agent workflow files
296
308
 
297
309
  mdkg recognizes a small set of canonical agent workflow documents:
@@ -323,10 +335,12 @@ By default, init/upgrade ignore generated raw archive source copies with `.mdkg/
323
335
 
324
336
  This release includes:
325
337
  - `init --agent`
326
- - default ignore updates with `--no-update-ignores` for `.mdkg/index/`, `.mdkg/pack/`, and raw archive source copies
338
+ - default ignore updates with `--no-update-ignores` for generated JSON index/temp/lock files, `.mdkg/pack/`, and raw archive source copies
327
339
  - root-only published init seed config
328
340
  - skills indexing and search/show/list support
329
341
  - JSON capability cache for skills, `SPEC.md`, `WORK.md`, core docs, and design docs
342
+ - SQLite index backend for fresh workspaces using built-in `node:sqlite`
343
+ - mutation locking and atomic writes for parallel mdkg calls
330
344
  - optional `skills: [...]` on work items
331
345
  - pack-time skill inclusion
332
346
  - latest-checkpoint resolver + index hint
@@ -360,7 +374,8 @@ Design and decision records live in the internal graph under `.mdkg/design/`.
360
374
  mdkg is not a secret store.
361
375
 
362
376
  Use these defaults:
363
- - keep `.mdkg/index/` gitignored
377
+ - keep generated `.mdkg/index/*.json`, temp, lock, WAL, SHM, and journal files gitignored
378
+ - commit `.mdkg/index/mdkg.sqlite` only when the repo intentionally tracks a reasonably sized rebuildable access cache
364
379
  - keep `.mdkg/pack/` gitignored
365
380
  - keep `.mdkg/archive/**/source/` gitignored unless a repo intentionally commits raw local copies
366
381
  - commit archive sidecar `.md` metadata and deterministic `.zip` caches when they are needed for reviewable evidence
package/dist/cli.js CHANGED
@@ -171,6 +171,8 @@ function printIndexHelp(log) {
171
171
  log(" - .mdkg/index/global.json");
172
172
  log(" - .mdkg/index/skills.json");
173
173
  log(" - .mdkg/index/capabilities.json");
174
+ log(" - .mdkg/index/imports.json when bundle imports are configured");
175
+ log(" - .mdkg/index/mdkg.sqlite when index.backend is sqlite");
174
176
  printGlobalOptions(log);
175
177
  }
176
178
  function printShowHelp(log) {
@@ -544,6 +546,7 @@ function printDoctorHelp(log) {
544
546
  log(" - Bundle import health and staleness");
545
547
  log(" - Index load/rebuild health");
546
548
  log(" - Capability cache load/rebuild health");
549
+ log(" - SQLite cache health when enabled");
547
550
  log("\nOptions:");
548
551
  log(" --json Emit machine-readable JSON output");
549
552
  printGlobalOptions(log);
@@ -3,10 +3,10 @@ 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.runArchiveAddCommand = runArchiveAddCommand;
7
6
  exports.runArchiveListCommand = runArchiveListCommand;
8
7
  exports.runArchiveShowCommand = runArchiveShowCommand;
9
8
  exports.runArchiveVerifyCommand = runArchiveVerifyCommand;
9
+ exports.runArchiveAddCommand = runArchiveAddCommand;
10
10
  exports.runArchiveCompressCommand = runArchiveCompressCommand;
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
@@ -15,11 +15,14 @@ const frontmatter_1 = require("../graph/frontmatter");
15
15
  const archive_integrity_1 = require("../graph/archive_integrity");
16
16
  const indexer_1 = require("../graph/indexer");
17
17
  const index_cache_1 = require("../graph/index_cache");
18
+ const reindex_1 = require("../graph/reindex");
18
19
  const visibility_1 = require("../graph/visibility");
19
20
  const date_1 = require("../util/date");
20
21
  const errors_1 = require("../util/errors");
21
22
  const id_1 = require("../util/id");
22
23
  const refs_1 = require("../util/refs");
24
+ const atomic_1 = require("../util/atomic");
25
+ const lock_1 = require("../util/lock");
23
26
  const zip_1 = require("../util/zip");
24
27
  const event_support_1 = require("./event_support");
25
28
  const ARCHIVE_KINDS = new Set(["source", "artifact"]);
@@ -124,8 +127,7 @@ function maybeReindex(root) {
124
127
  if (!config.index.auto_reindex) {
125
128
  return;
126
129
  }
127
- const outputPath = path_1.default.resolve(root, config.index.global_index_path);
128
- (0, index_cache_1.writeIndex)(outputPath, (0, indexer_1.buildIndex)(root, config, { tolerant: config.index.tolerant }));
130
+ (0, reindex_1.writeDerivedIndexes)(root, config, (0, indexer_1.buildIndex)(root, config, { tolerant: config.index.tolerant }));
129
131
  }
130
132
  function resolveArchiveNode(root, id, ws) {
131
133
  const config = (0, config_1.loadConfig)(root);
@@ -174,7 +176,7 @@ function stringAttribute(value) {
174
176
  function writeArchiveSidecar(sidecarPath, frontmatter, body) {
175
177
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter);
176
178
  const content = ["---", ...lines, "---", body.trimStart()].join("\n");
177
- fs_1.default.writeFileSync(sidecarPath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
179
+ (0, atomic_1.atomicWriteFile)(sidecarPath, content.endsWith("\n") ? content : `${content}\n`);
178
180
  }
179
181
  function verifyArchiveSidecar(root, ws, sidecarPath) {
180
182
  const relativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
@@ -276,7 +278,7 @@ function loadArchiveVerifyResults(options) {
276
278
  }
277
279
  return results;
278
280
  }
279
- function runArchiveAddCommand(options) {
281
+ function runArchiveAddCommandLocked(options) {
280
282
  const config = (0, config_1.loadConfig)(options.root);
281
283
  const ws = normalizeWorkspace(options.ws);
282
284
  const workspace = config.workspaces[ws];
@@ -311,7 +313,7 @@ function runArchiveAddCommand(options) {
311
313
  fs_1.default.copyFileSync(sourcePath, rawPath);
312
314
  const rawData = fs_1.default.readFileSync(rawPath);
313
315
  const zipData = (0, zip_1.createDeterministicZip)(basename, rawData);
314
- fs_1.default.writeFileSync(zipPath, zipData);
316
+ (0, atomic_1.atomicWriteFile)(zipPath, zipData);
315
317
  const frontmatter = {
316
318
  id,
317
319
  type: "archive",
@@ -423,7 +425,7 @@ function runArchiveVerifyCommand(options) {
423
425
  throw new errors_1.ValidationError("archive verification failed");
424
426
  }
425
427
  }
426
- function runArchiveCompressCommand(options) {
428
+ function runArchiveCompressCommandLocked(options) {
427
429
  if (!options.all && !options.id) {
428
430
  throw new errors_1.UsageError("archive compress requires <id-or-archive-uri> or --all");
429
431
  }
@@ -441,7 +443,7 @@ function runArchiveCompressCommand(options) {
441
443
  }
442
444
  const rawData = fs_1.default.readFileSync(rawPath);
443
445
  const zipData = (0, zip_1.createDeterministicZip)(path_1.default.basename(rawPath), rawData);
444
- fs_1.default.writeFileSync(zipPath, zipData);
446
+ (0, atomic_1.atomicWriteFile)(zipPath, zipData);
445
447
  const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(sidecarPath, "utf8"), sidecarPath);
446
448
  const nextFrontmatter = {
447
449
  ...parsed.frontmatter,
@@ -472,3 +474,13 @@ function runArchiveCompressCommand(options) {
472
474
  }
473
475
  console.log(`archive compressed: ${updated.length}`);
474
476
  }
477
+ function withArchiveLock(root, fn) {
478
+ const config = (0, config_1.loadConfig)(root);
479
+ return (0, lock_1.withMutationLock)(root, config.index.lock_timeout_ms, fn);
480
+ }
481
+ function runArchiveAddCommand(options) {
482
+ return withArchiveLock(options.root, () => runArchiveAddCommandLocked(options));
483
+ }
484
+ function runArchiveCompressCommand(options) {
485
+ return withArchiveLock(options.root, () => runArchiveCompressCommandLocked(options));
486
+ }
@@ -16,6 +16,8 @@ const migrate_1 = require("../core/migrate");
16
16
  const workspace_path_1 = require("../core/workspace_path");
17
17
  const bundle_imports_1 = require("../graph/bundle_imports");
18
18
  const errors_1 = require("../util/errors");
19
+ const atomic_1 = require("../util/atomic");
20
+ const lock_1 = require("../util/lock");
19
21
  const ALIAS_RE = /^[a-z][a-z0-9_]*$/;
20
22
  function writeJson(value) {
21
23
  console.log(JSON.stringify(value, null, 2));
@@ -71,7 +73,7 @@ function readRawConfig(root) {
71
73
  return { configPath, raw: migrated };
72
74
  }
73
75
  function writeRawConfig(configPath, raw) {
74
- fs_1.default.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
76
+ (0, atomic_1.atomicWriteFile)(configPath, `${JSON.stringify(raw, null, 2)}\n`);
75
77
  }
76
78
  function getImports(raw) {
77
79
  const imports = raw.bundle_imports;
@@ -98,7 +100,11 @@ function healthByAlias(root, alias) {
98
100
  }
99
101
  return health;
100
102
  }
101
- function runBundleImportAddCommand(options) {
103
+ function withBundleImportLock(root, fn) {
104
+ const config = (0, config_1.loadConfig)(root);
105
+ return (0, lock_1.withMutationLock)(root, config.index.lock_timeout_ms, fn);
106
+ }
107
+ function runBundleImportAddCommandLocked(options) {
102
108
  const alias = normalizeAlias(options.alias);
103
109
  const bundlePath = normalizeContained(options.bundlePath, "bundle import path");
104
110
  const visibility = normalizeVisibility(options.visibility);
@@ -150,6 +156,9 @@ function runBundleImportAddCommand(options) {
150
156
  console.log(`warnings: ${health.warning_count}`);
151
157
  }
152
158
  }
159
+ function runBundleImportAddCommand(options) {
160
+ return withBundleImportLock(options.root, () => runBundleImportAddCommandLocked(options));
161
+ }
153
162
  function runBundleImportListCommand(options) {
154
163
  const config = (0, config_1.loadConfig)(options.root);
155
164
  const imports = (0, bundle_imports_1.buildBundleImportsIndex)(options.root, config).index.imports;
@@ -166,7 +175,7 @@ function runBundleImportListCommand(options) {
166
175
  console.log(`${item.alias} | ${status} | ${item.visibility} | ${item.path}`);
167
176
  }
168
177
  }
169
- function runBundleImportRemoveCommand(options) {
178
+ function runBundleImportRemoveCommandLocked(options) {
170
179
  const alias = normalizeAlias(options.alias);
171
180
  const { configPath, raw } = readRawConfig(options.root);
172
181
  const imports = getImports(raw);
@@ -185,7 +194,10 @@ function runBundleImportRemoveCommand(options) {
185
194
  }
186
195
  console.log(`bundle import removed: ${alias}`);
187
196
  }
188
- function setBundleImportEnabled(options, enabled) {
197
+ function runBundleImportRemoveCommand(options) {
198
+ return withBundleImportLock(options.root, () => runBundleImportRemoveCommandLocked(options));
199
+ }
200
+ function setBundleImportEnabledLocked(options, enabled) {
189
201
  const alias = normalizeAlias(options.alias);
190
202
  const { configPath, raw } = readRawConfig(options.root);
191
203
  const imports = getImports(raw);
@@ -206,10 +218,10 @@ function setBundleImportEnabled(options, enabled) {
206
218
  console.log(`bundle import ${enabled ? "enabled" : "disabled"}: ${alias}`);
207
219
  }
208
220
  function runBundleImportEnableCommand(options) {
209
- setBundleImportEnabled(options, true);
221
+ withBundleImportLock(options.root, () => setBundleImportEnabledLocked(options, true));
210
222
  }
211
223
  function runBundleImportDisableCommand(options) {
212
- setBundleImportEnabled(options, false);
224
+ withBundleImportLock(options.root, () => setBundleImportEnabledLocked(options, false));
213
225
  }
214
226
  function runBundleImportVerifyCommand(options) {
215
227
  const config = (0, config_1.loadConfig)(options.root);
@@ -13,6 +13,9 @@ const loader_1 = require("../templates/loader");
13
13
  const date_1 = require("../util/date");
14
14
  const errors_1 = require("../util/errors");
15
15
  const id_1 = require("../util/id");
16
+ const atomic_1 = require("../util/atomic");
17
+ const lock_1 = require("../util/lock");
18
+ const sqlite_index_1 = require("../graph/sqlite_index");
16
19
  const event_support_1 = require("./event_support");
17
20
  function parseCsvList(raw) {
18
21
  if (!raw) {
@@ -38,6 +41,9 @@ function normalizeIdRef(value, key) {
38
41
  return normalized;
39
42
  }
40
43
  function nextCheckpointId(index, ws) {
44
+ return `chk-${maxCheckpointId(index, ws) + 1}`;
45
+ }
46
+ function maxCheckpointId(index, ws) {
41
47
  let max = 0;
42
48
  for (const node of Object.values(index.nodes)) {
43
49
  if (node.ws !== ws) {
@@ -52,7 +58,7 @@ function nextCheckpointId(index, ws) {
52
58
  max = parsed;
53
59
  }
54
60
  }
55
- return `chk-${max + 1}`;
61
+ return max;
56
62
  }
57
63
  function slugifyTitle(title) {
58
64
  const slug = title
@@ -77,7 +83,7 @@ function normalizeWorkspace(value) {
77
83
  }
78
84
  return normalized;
79
85
  }
80
- function createCheckpoint(options) {
86
+ function createCheckpointLocked(options) {
81
87
  const title = options.title.trim();
82
88
  if (!title) {
83
89
  throw new errors_1.UsageError("checkpoint title cannot be empty");
@@ -99,7 +105,15 @@ function createCheckpoint(options) {
99
105
  throw new errors_1.UsageError(`--priority must be between ${priorityMin} and ${priorityMax}`);
100
106
  }
101
107
  const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
102
- const id = nextCheckpointId(index, ws);
108
+ const id = (0, sqlite_index_1.isSqliteBackend)(config)
109
+ ? (0, sqlite_index_1.reserveSqliteNumericId)({
110
+ root: options.root,
111
+ config,
112
+ ws,
113
+ prefix: "chk",
114
+ currentMax: maxCheckpointId(index, ws),
115
+ }) ?? nextCheckpointId(index, ws)
116
+ : nextCheckpointId(index, ws);
103
117
  const slug = slugifyTitle(title);
104
118
  const fileName = `${id}-${slug}.md`;
105
119
  const wsEntry = config.workspaces[ws];
@@ -129,8 +143,16 @@ function createCheckpoint(options) {
129
143
  relates,
130
144
  scope,
131
145
  });
132
- fs_1.default.mkdirSync(workDir, { recursive: true });
133
- fs_1.default.writeFileSync(filePath, content, "utf8");
146
+ try {
147
+ (0, atomic_1.writeFileExclusive)(filePath, content);
148
+ }
149
+ catch (err) {
150
+ const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
151
+ if (code === "EEXIST") {
152
+ throw new errors_1.UsageError(`checkpoint file already exists: ${path_1.default.relative(options.root, filePath)}`);
153
+ }
154
+ throw err;
155
+ }
134
156
  (0, event_support_1.appendAutomaticEvent)({
135
157
  root: options.root,
136
158
  ws,
@@ -148,6 +170,10 @@ function createCheckpoint(options) {
148
170
  path: path_1.default.relative(options.root, filePath),
149
171
  };
150
172
  }
173
+ function createCheckpoint(options) {
174
+ const config = (0, config_1.loadConfig)(options.root);
175
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => createCheckpointLocked(options));
176
+ }
151
177
  function runCheckpointNewCommand(options) {
152
178
  const checkpoint = createCheckpoint(options);
153
179
  if (options.json) {
@@ -13,31 +13,37 @@ const bundle_imports_1 = require("../graph/bundle_imports");
13
13
  const node_1 = require("../graph/node");
14
14
  const template_schema_1 = require("../graph/template_schema");
15
15
  const visibility_1 = require("../graph/visibility");
16
+ const sqlite_index_1 = require("../graph/sqlite_index");
16
17
  const errors_1 = require("../util/errors");
17
- const REQUIRED_NODE_MAJOR = 18;
18
+ const REQUIRED_NODE_MAJOR = 24;
19
+ const REQUIRED_NODE_MINOR = 15;
18
20
  const ARCHIVE_RAW_ALLOWED_DIRS = new Set(["source"]);
19
- function parseNodeMajor(version) {
20
- const major = Number.parseInt(version.split(".")[0] ?? "", 10);
21
- if (!Number.isInteger(major)) {
21
+ function parseNodeVersion(version) {
22
+ const [majorRaw, minorRaw, patchRaw] = version.split(".");
23
+ const major = Number.parseInt(majorRaw ?? "", 10);
24
+ const minor = Number.parseInt(minorRaw ?? "", 10);
25
+ const patch = Number.parseInt(patchRaw ?? "", 10);
26
+ if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) {
22
27
  return null;
23
28
  }
24
- return major;
29
+ return { major, minor, patch };
25
30
  }
26
31
  function runNodeVersionCheck() {
27
32
  const nodeVersion = process.versions.node;
28
- const major = parseNodeMajor(nodeVersion);
29
- if (major === null) {
33
+ const parsed = parseNodeVersion(nodeVersion);
34
+ if (parsed === null) {
30
35
  return {
31
36
  name: "node-version",
32
37
  ok: false,
33
38
  detail: `unable to parse Node.js version: ${nodeVersion}`,
34
39
  };
35
40
  }
36
- if (major < REQUIRED_NODE_MAJOR) {
41
+ if (parsed.major < REQUIRED_NODE_MAJOR ||
42
+ (parsed.major === REQUIRED_NODE_MAJOR && parsed.minor < REQUIRED_NODE_MINOR)) {
37
43
  return {
38
44
  name: "node-version",
39
45
  ok: false,
40
- detail: `Node.js ${nodeVersion} is unsupported (requires >=${REQUIRED_NODE_MAJOR})`,
46
+ detail: `Node.js ${nodeVersion} is unsupported (requires >=${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}.0)`,
41
47
  };
42
48
  }
43
49
  return {
@@ -46,6 +52,36 @@ function runNodeVersionCheck() {
46
52
  detail: `Node.js ${nodeVersion} (ok)`,
47
53
  };
48
54
  }
55
+ function runSqliteCheck(root, config) {
56
+ if (!(0, sqlite_index_1.isSqliteBackend)(config)) {
57
+ return {
58
+ name: "sqlite-cache",
59
+ ok: true,
60
+ detail: "SQLite backend disabled; JSON cache backend active",
61
+ };
62
+ }
63
+ const health = (0, sqlite_index_1.sqliteHealth)(root, config);
64
+ if (health.errors.length > 0) {
65
+ return {
66
+ name: "sqlite-cache",
67
+ ok: false,
68
+ detail: health.errors.join("; "),
69
+ };
70
+ }
71
+ if (health.warnings.length > 0) {
72
+ return {
73
+ name: "sqlite-cache",
74
+ ok: true,
75
+ level: "warn",
76
+ detail: health.warnings.join("; "),
77
+ };
78
+ }
79
+ return {
80
+ name: "sqlite-cache",
81
+ ok: true,
82
+ detail: `.mdkg SQLite cache ok (${health.size} bytes)`,
83
+ };
84
+ }
49
85
  function walkFiles(root) {
50
86
  if (!fs_1.default.existsSync(root)) {
51
87
  return [];
@@ -262,6 +298,7 @@ function runDoctorCommand(options) {
262
298
  results.push(runArchiveStorageCheck(options.root));
263
299
  results.push(runArchiveLargeCacheCheck(options.root, config.archive.large_cache_warning_bytes));
264
300
  results.push(runBundleStorageCheck(options.root, config.bundles.output_dir));
301
+ results.push(runSqliteCheck(options.root, config));
265
302
  results.push(...runBundleImportChecks(options.root, config));
266
303
  results.push(runVisibilityPolicyCheck(options.root, config, options));
267
304
  try {
@@ -6,29 +6,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runIndexCommand = runIndexCommand;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const config_1 = require("../core/config");
9
- const indexer_1 = require("../graph/indexer");
10
- const index_cache_1 = require("../graph/index_cache");
11
- const skills_index_cache_1 = require("../graph/skills_index_cache");
12
- const skills_indexer_1 = require("../graph/skills_indexer");
13
- const capabilities_indexer_1 = require("../graph/capabilities_indexer");
14
- const capabilities_index_cache_1 = require("../graph/capabilities_index_cache");
15
- const bundle_imports_1 = require("../graph/bundle_imports");
9
+ const reindex_1 = require("../graph/reindex");
10
+ const lock_1 = require("../util/lock");
16
11
  function runIndexCommand(options) {
17
12
  const config = (0, config_1.loadConfig)(options.root);
18
- const nodeIndex = (0, indexer_1.buildIndex)(options.root, config, { tolerant: options.tolerant });
19
- const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(options.root, config);
20
- const capabilitiesIndex = (0, capabilities_indexer_1.buildCapabilitiesIndex)(options.root, config, nodeIndex);
21
- const importsIndex = (0, bundle_imports_1.buildBundleImportsIndex)(options.root, config);
22
- const nodesOutputPath = path_1.default.resolve(options.root, config.index.global_index_path);
23
- const skillsOutputPath = (0, skills_indexer_1.resolveSkillsIndexPath)(options.root);
24
- const capabilitiesOutputPath = (0, capabilities_indexer_1.resolveCapabilitiesIndexPath)(options.root, config);
25
- const importsOutputPath = (0, bundle_imports_1.resolveBundleImportsIndexPath)(options.root);
26
- (0, index_cache_1.writeIndex)(nodesOutputPath, nodeIndex);
27
- (0, skills_index_cache_1.writeSkillsIndex)(skillsOutputPath, skillsIndex);
28
- (0, capabilities_index_cache_1.writeCapabilitiesIndex)(capabilitiesOutputPath, capabilitiesIndex);
29
- (0, bundle_imports_1.writeBundleImportsIndex)(importsOutputPath, importsIndex.index);
30
- console.log(`index written: ${path_1.default.relative(options.root, nodesOutputPath)}`);
31
- console.log(`skills index written: ${path_1.default.relative(options.root, skillsOutputPath)}`);
32
- console.log(`capabilities index written: ${path_1.default.relative(options.root, capabilitiesOutputPath)}`);
33
- console.log(`bundle imports index written: ${path_1.default.relative(options.root, importsOutputPath)}`);
13
+ (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
14
+ const result = (0, reindex_1.writeDerivedIndexes)(options.root, config, undefined, { tolerant: options.tolerant });
15
+ console.log(`index written: ${path_1.default.relative(options.root, result.paths.nodes)}`);
16
+ console.log(`skills index written: ${path_1.default.relative(options.root, result.paths.skills)}`);
17
+ console.log(`capabilities index written: ${path_1.default.relative(options.root, result.paths.capabilities)}`);
18
+ console.log(`bundle imports index written: ${path_1.default.relative(options.root, result.paths.imports)}`);
19
+ if (result.paths.sqlite) {
20
+ console.log(`sqlite index written: ${path_1.default.relative(options.root, result.paths.sqlite)}`);
21
+ }
22
+ });
34
23
  }
@@ -409,7 +409,13 @@ function runInitCommand(options) {
409
409
  try {
410
410
  if (shouldUpdateGitignore) {
411
411
  if (appendIgnoreEntries(path_1.default.join(root, ".gitignore"), [
412
- ".mdkg/index/",
412
+ ".mdkg/index/*.json",
413
+ ".mdkg/index/*.tmp",
414
+ ".mdkg/index/*.lock",
415
+ ".mdkg/index/write.lock/",
416
+ ".mdkg/index/*.sqlite-wal",
417
+ ".mdkg/index/*.sqlite-shm",
418
+ ".mdkg/index/*.sqlite-journal",
413
419
  ".mdkg/pack/",
414
420
  ".mdkg/archive/**/source/",
415
421
  ])) {
@@ -16,6 +16,10 @@ const date_1 = require("../util/date");
16
16
  const errors_1 = require("../util/errors");
17
17
  const qid_1 = require("../util/qid");
18
18
  const id_1 = require("../util/id");
19
+ const atomic_1 = require("../util/atomic");
20
+ const lock_1 = require("../util/lock");
21
+ const sqlite_index_1 = require("../graph/sqlite_index");
22
+ const reindex_1 = require("../graph/reindex");
19
23
  const event_support_1 = require("./event_support");
20
24
  const DEC_ID_RE = /^dec-[0-9]+$/;
21
25
  const DEC_STATUS = new Set(["proposed", "accepted", "rejected", "superseded"]);
@@ -97,6 +101,9 @@ function slugifyTitle(title) {
97
101
  return slug.length > maxLen ? slug.slice(0, maxLen).replace(/-+$/g, "") : slug;
98
102
  }
99
103
  function nextIdForPrefix(index, ws, prefix) {
104
+ return `${prefix}-${maxIdForPrefix(index, ws, prefix) + 1}`;
105
+ }
106
+ function maxIdForPrefix(index, ws, prefix) {
100
107
  let max = 0;
101
108
  const pattern = new RegExp(`^${prefix}-(\\d+)$`);
102
109
  for (const node of Object.values(index)) {
@@ -112,7 +119,7 @@ function nextIdForPrefix(index, ws, prefix) {
112
119
  max = parsed;
113
120
  }
114
121
  }
115
- return `${prefix}-${max + 1}`;
122
+ return max;
116
123
  }
117
124
  function idPrefixForType(type) {
118
125
  if (type === "checkpoint") {
@@ -150,7 +157,7 @@ function ensureExists(index, value, ws, label) {
150
157
  throw new errors_1.NotFoundError((0, qid_1.formatResolveError)(label, value, resolved, ws));
151
158
  }
152
159
  }
153
- function runNewCommand(options) {
160
+ function runNewCommandLocked(options) {
154
161
  const title = options.title.trim();
155
162
  if (!title) {
156
163
  throw new errors_1.UsageError("title cannot be empty");
@@ -181,7 +188,15 @@ function runNewCommand(options) {
181
188
  const prefix = idPrefixForType(type);
182
189
  const id = options.id !== undefined
183
190
  ? normalizeAgentFileId(options.id)
184
- : nextIdForPrefix(index.nodes, ws, prefix);
191
+ : (0, sqlite_index_1.isSqliteBackend)(config)
192
+ ? (0, sqlite_index_1.reserveSqliteNumericId)({
193
+ root: options.root,
194
+ config,
195
+ ws,
196
+ prefix,
197
+ currentMax: maxIdForPrefix(index.nodes, ws, prefix),
198
+ }) ?? nextIdForPrefix(index.nodes, ws, prefix)
199
+ : nextIdForPrefix(index.nodes, ws, prefix);
185
200
  if (index.nodes[`${ws}:${id}`]) {
186
201
  throw new errors_1.UsageError(`node already exists: ${ws}:${id}`);
187
202
  }
@@ -317,12 +332,19 @@ function runNewCommand(options) {
317
332
  created: today,
318
333
  updated: today,
319
334
  });
320
- fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
321
- fs_1.default.writeFileSync(filePath, content, "utf8");
335
+ try {
336
+ (0, atomic_1.writeFileExclusive)(filePath, content);
337
+ }
338
+ catch (err) {
339
+ const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
340
+ if (code === "EEXIST") {
341
+ throw new errors_1.UsageError(`node already exists: ${path_1.default.relative(options.root, filePath)}`);
342
+ }
343
+ throw err;
344
+ }
322
345
  if (config.index.auto_reindex && !noReindex) {
323
346
  const updatedIndex = (0, indexer_1.buildIndex)(options.root, config, { tolerant: config.index.tolerant });
324
- const outputPath = path_1.default.resolve(options.root, config.index.global_index_path);
325
- (0, index_cache_1.writeIndex)(outputPath, updatedIndex);
347
+ (0, reindex_1.writeDerivedIndexes)(options.root, config, updatedIndex);
326
348
  }
327
349
  (0, event_support_1.appendAutomaticEvent)({
328
350
  root: options.root,
@@ -354,3 +376,7 @@ function runNewCommand(options) {
354
376
  }
355
377
  console.log(`node created: ${receipt.qid} (${receipt.path})`);
356
378
  }
379
+ function runNewCommand(options) {
380
+ const config = (0, config_1.loadConfig)(options.root);
381
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runNewCommandLocked(options));
382
+ }