mdkg 0.1.6 → 0.1.8

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.
@@ -11,6 +11,7 @@ const migrate_1 = require("../core/migrate");
11
11
  const errors_1 = require("../util/errors");
12
12
  const date_1 = require("../util/date");
13
13
  const version_1 = require("../core/version");
14
+ const project_db_1 = require("../core/project_db");
14
15
  const init_manifest_1 = require("./init_manifest");
15
16
  const skill_support_1 = require("./skill_support");
16
17
  const skill_mirror_1 = require("./skill_mirror");
@@ -416,8 +417,10 @@ function runInitCommand(options) {
416
417
  ".mdkg/index/*.sqlite-wal",
417
418
  ".mdkg/index/*.sqlite-shm",
418
419
  ".mdkg/index/*.sqlite-journal",
420
+ ...project_db_1.PROJECT_DB_GITIGNORE_ENTRIES,
419
421
  ".mdkg/state/",
420
422
  ".mdkg/pack/",
423
+ ".mdkg/subgraphs/",
421
424
  ".mdkg/archive/**/source/",
422
425
  ])) {
423
426
  stats.ignoreFilesUpdated.push(".gitignore");
@@ -16,6 +16,7 @@ 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 refs_1 = require("../util/refs");
19
20
  const atomic_1 = require("../util/atomic");
20
21
  const lock_1 = require("../util/lock");
21
22
  const sqlite_index_1 = require("../graph/sqlite_index");
@@ -44,7 +45,7 @@ function normalizeId(value, key) {
44
45
  }
45
46
  function normalizeIdRef(value, key) {
46
47
  const normalized = value.toLowerCase();
47
- if (!(0, id_1.isCanonicalIdRef)(normalized)) {
48
+ if (!(0, id_1.isPortableIdRef)(normalized)) {
48
49
  throw new errors_1.UsageError(`${key} entries must match <id> or <ws>:<id>: ${value}`);
49
50
  }
50
51
  return normalized;
@@ -65,6 +66,16 @@ function normalizeLowercaseList(raw) {
65
66
  function normalizeIdList(raw, key) {
66
67
  return parseCsvList(raw).map((value) => normalizeId(value, key));
67
68
  }
69
+ function normalizeRef(value, key) {
70
+ const normalized = value.includes("://") ? value : value.toLowerCase();
71
+ if (!(0, refs_1.validatePortableOrUriRef)(normalized)) {
72
+ throw new errors_1.UsageError(`${key} entries must be portable ids, qids, or URI refs: ${value}`);
73
+ }
74
+ return normalized;
75
+ }
76
+ function normalizeRefList(raw, key) {
77
+ return parseCsvList(raw).map((value) => normalizeRef(value, key));
78
+ }
68
79
  function normalizeIdRefList(raw, key) {
69
80
  return parseCsvList(raw).map((value) => normalizeIdRef(value, key));
70
81
  }
@@ -156,6 +167,10 @@ function ensureExists(index, value, ws, label) {
156
167
  if (resolved.status !== "ok") {
157
168
  throw new errors_1.NotFoundError((0, qid_1.formatResolveError)(label, value, resolved, ws));
158
169
  }
170
+ const node = index.nodes[resolved.qid];
171
+ if (["epic", "parent", "prev", "next"].includes(label) && node?.source?.imported) {
172
+ throw new errors_1.UsageError(`${label} cannot target read-only subgraph node ${node.qid}`);
173
+ }
159
174
  }
160
175
  function runNewCommandLocked(options) {
161
176
  const title = options.title.trim();
@@ -259,7 +274,7 @@ function runNewCommandLocked(options) {
259
274
  const relates = normalizeIdRefList(options.relates, "--relates");
260
275
  const blockedBy = normalizeIdRefList(options.blockedBy, "--blocked-by");
261
276
  const blocks = normalizeIdRefList(options.blocks, "--blocks");
262
- const refs = normalizeIdList(options.refs, "--refs");
277
+ const refs = normalizeRefList(options.refs, "--refs");
263
278
  const aliases = normalizeLowercaseList(options.aliases);
264
279
  const tags = normalizeLowercaseList(options.tags);
265
280
  const owners = normalizeLowercaseList(options.owners);
@@ -11,8 +11,11 @@ exports.runSubgraphEnableCommand = runSubgraphEnableCommand;
11
11
  exports.runSubgraphDisableCommand = runSubgraphDisableCommand;
12
12
  exports.runSubgraphVerifyCommand = runSubgraphVerifyCommand;
13
13
  exports.runSubgraphRefreshCommand = runSubgraphRefreshCommand;
14
+ exports.runSubgraphSyncCommand = runSubgraphSyncCommand;
15
+ exports.runSubgraphMaterializeCommand = runSubgraphMaterializeCommand;
14
16
  const fs_1 = __importDefault(require("fs"));
15
17
  const path_1 = __importDefault(require("path"));
18
+ const child_process_1 = require("child_process");
16
19
  const config_1 = require("../core/config");
17
20
  const migrate_1 = require("../core/migrate");
18
21
  const workspace_path_1 = require("../core/workspace_path");
@@ -21,6 +24,7 @@ const reindex_1 = require("../graph/reindex");
21
24
  const errors_1 = require("../util/errors");
22
25
  const atomic_1 = require("../util/atomic");
23
26
  const lock_1 = require("../util/lock");
27
+ const bundle_1 = require("./bundle");
24
28
  const ALIAS_RE = /^[a-z][a-z0-9_]*$/;
25
29
  function writeJson(value) {
26
30
  console.log(JSON.stringify(value, null, 2));
@@ -56,6 +60,25 @@ function normalizeContained(value, label) {
56
60
  throw new errors_1.UsageError(err instanceof Error ? err.message : String(err));
57
61
  }
58
62
  }
63
+ function toPosixPath(value) {
64
+ return value.split(path_1.default.sep).join("/");
65
+ }
66
+ function relativeToRoot(root, filePath) {
67
+ return toPosixPath(path_1.default.relative(root, filePath));
68
+ }
69
+ function isObject(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+ function readPackageVersion() {
73
+ const packagePath = path_1.default.resolve(__dirname, "..", "..", "package.json");
74
+ try {
75
+ const raw = JSON.parse(fs_1.default.readFileSync(packagePath, "utf8"));
76
+ return typeof raw.version === "string" ? raw.version : "unknown";
77
+ }
78
+ catch {
79
+ return "unknown";
80
+ }
81
+ }
59
82
  function readRawConfig(root) {
60
83
  const configPath = path_1.default.join(root, ".mdkg", "config.json");
61
84
  if (!fs_1.default.existsSync(configPath)) {
@@ -106,6 +129,19 @@ function healthByAlias(root, alias) {
106
129
  }
107
130
  return health;
108
131
  }
132
+ function selectAliases(config, alias, all) {
133
+ if (alias && all) {
134
+ throw new errors_1.UsageError("choose either an alias or --all, not both");
135
+ }
136
+ if (alias) {
137
+ const normalized = normalizeAlias(alias);
138
+ if (!config.subgraphs[normalized]) {
139
+ throw new errors_1.NotFoundError(`subgraph not found: ${normalized}`);
140
+ }
141
+ return [normalized];
142
+ }
143
+ return Object.keys(config.subgraphs).sort();
144
+ }
109
145
  function withSubgraphLock(root, fn) {
110
146
  const config = (0, config_1.loadConfig)(root);
111
147
  return (0, lock_1.withMutationLock)(root, config.index.lock_timeout_ms, fn);
@@ -310,3 +346,383 @@ function runSubgraphRefreshCommand(options) {
310
346
  }
311
347
  });
312
348
  }
349
+ function gitRun(cwd, args) {
350
+ const result = (0, child_process_1.spawnSync)("git", args, { cwd, encoding: "utf8", stdio: "pipe" });
351
+ return {
352
+ ok: result.status === 0,
353
+ stdout: result.stdout.replace(/\r?\n$/, ""),
354
+ stderr: result.stderr.trim(),
355
+ };
356
+ }
357
+ function sameRealPath(a, b) {
358
+ try {
359
+ return fs_1.default.realpathSync(a) === fs_1.default.realpathSync(b);
360
+ }
361
+ catch {
362
+ return path_1.default.resolve(a) === path_1.default.resolve(b);
363
+ }
364
+ }
365
+ function isPathWithin(parent, child) {
366
+ const relative = path_1.default.relative(path_1.default.resolve(parent), path_1.default.resolve(child));
367
+ return relative === "" || (!relative.startsWith("..") && !path_1.default.isAbsolute(relative));
368
+ }
369
+ function inspectSourcePath(root, alias, subgraph, allowDirty) {
370
+ if (!subgraph.source_path) {
371
+ throw new errors_1.UsageError(`subgraph ${alias} is missing source_path`);
372
+ }
373
+ const sourcePath = normalizeContained(subgraph.source_path, `subgraphs.${alias}.source_path`);
374
+ const sourceRoot = path_1.default.resolve(root, sourcePath);
375
+ if (!fs_1.default.existsSync(sourceRoot) || !fs_1.default.statSync(sourceRoot).isDirectory()) {
376
+ throw new errors_1.NotFoundError(`subgraph ${alias} source_path does not exist: ${sourcePath}`);
377
+ }
378
+ const gitTop = gitRun(sourceRoot, ["rev-parse", "--show-toplevel"]);
379
+ const gitTopPath = gitTop.stdout.trim();
380
+ if (!gitTop.ok || !gitTopPath) {
381
+ throw new errors_1.UsageError(`subgraph ${alias} source_path is not a Git repo: ${sourcePath}`);
382
+ }
383
+ if (!sameRealPath(gitTopPath, sourceRoot)) {
384
+ throw new errors_1.UsageError(`subgraph ${alias} source_path must be the child Git repo root: ${sourcePath}`);
385
+ }
386
+ const mdkgDir = path_1.default.join(sourceRoot, ".mdkg");
387
+ if (!fs_1.default.existsSync(mdkgDir) || !fs_1.default.statSync(mdkgDir).isDirectory()) {
388
+ throw new errors_1.NotFoundError(`subgraph ${alias} source_path is missing .mdkg: ${sourcePath}`);
389
+ }
390
+ const head = gitRun(sourceRoot, ["rev-parse", "HEAD"]);
391
+ const headValue = head.stdout.trim();
392
+ if (!head.ok || !headValue) {
393
+ throw new errors_1.UsageError(`subgraph ${alias} source Git HEAD could not be read`);
394
+ }
395
+ const branchResult = gitRun(sourceRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
396
+ const branchValue = branchResult.stdout.trim();
397
+ const branch = branchResult.ok && branchValue && branchValue !== "HEAD"
398
+ ? branchValue
399
+ : "detached";
400
+ const status = gitRun(sourceRoot, ["status", "--porcelain", "--untracked-files=no"]);
401
+ if (!status.ok) {
402
+ throw new errors_1.UsageError(`subgraph ${alias} tracked dirty state could not be read`);
403
+ }
404
+ const dirtyTrackedPaths = status.stdout
405
+ ? status.stdout.split(/\r?\n/).map((line) => line.slice(3)).filter(Boolean).sort()
406
+ : [];
407
+ if (dirtyTrackedPaths.length > 0 && !allowDirty) {
408
+ throw new errors_1.UsageError(`subgraph ${alias} source_path has dirty tracked changes: ${dirtyTrackedPaths.join(", ")}`);
409
+ }
410
+ return {
411
+ sourceRoot,
412
+ branch,
413
+ head: headValue,
414
+ sourceRepo: `${branch}@${headValue}`,
415
+ dirtyTracked: dirtyTrackedPaths.length > 0,
416
+ dirtyTrackedPaths,
417
+ };
418
+ }
419
+ function existingBundleHash(bundlePath) {
420
+ if (!fs_1.default.existsSync(bundlePath)) {
421
+ return undefined;
422
+ }
423
+ try {
424
+ return (0, bundle_1.parseBundle)(bundlePath).manifest.bundle_hash;
425
+ }
426
+ catch {
427
+ return undefined;
428
+ }
429
+ }
430
+ function enabledSources(subgraph) {
431
+ return subgraph.sources.filter((source) => source.enabled);
432
+ }
433
+ function syncOneAlias(options) {
434
+ const warnings = [];
435
+ const errors = [];
436
+ const sources = [];
437
+ const receipt = {
438
+ alias: options.alias,
439
+ enabled: options.subgraph.enabled,
440
+ dry_run: options.dryRun,
441
+ source_path: options.subgraph.source_path,
442
+ old_source_repo: options.subgraph.source_repo,
443
+ updated: false,
444
+ skipped: false,
445
+ warnings,
446
+ errors,
447
+ sources,
448
+ };
449
+ if (!options.subgraph.enabled) {
450
+ receipt.skipped = true;
451
+ warnings.push("subgraph is disabled");
452
+ return receipt;
453
+ }
454
+ let gitState;
455
+ try {
456
+ gitState = inspectSourcePath(options.root, options.alias, options.subgraph, options.allowDirty);
457
+ receipt.new_source_repo = gitState.sourceRepo;
458
+ receipt.source_git_head = gitState.head;
459
+ receipt.dirty_tracked = gitState.dirtyTracked;
460
+ receipt.dirty_tracked_paths = gitState.dirtyTrackedPaths;
461
+ }
462
+ catch (err) {
463
+ errors.push(err instanceof Error ? err.message : String(err));
464
+ return receipt;
465
+ }
466
+ const activeSources = enabledSources(options.subgraph);
467
+ if (activeSources.length === 0) {
468
+ errors.push(`subgraph ${options.alias} has no enabled sources`);
469
+ return receipt;
470
+ }
471
+ const planned = [];
472
+ for (const source of activeSources) {
473
+ const outputPath = path_1.default.resolve(options.root, source.path);
474
+ const sourceReceipt = {
475
+ label: source.label,
476
+ path: source.path,
477
+ enabled: source.enabled,
478
+ expected_profile: source.expected_profile,
479
+ old_bundle_hash: existingBundleHash(outputPath),
480
+ old_zip_sha256: fs_1.default.existsSync(outputPath) ? (0, bundle_1.sha256Buffer)(fs_1.default.readFileSync(outputPath)) : undefined,
481
+ warnings: [],
482
+ errors: [],
483
+ };
484
+ sources.push(sourceReceipt);
485
+ if (isPathWithin(gitState.sourceRoot, outputPath)) {
486
+ sourceReceipt.errors.push(`bundle path must be root-owned and outside source_path: ${source.path}`);
487
+ errors.push(`source ${source.path}: ${sourceReceipt.errors[sourceReceipt.errors.length - 1]}`);
488
+ continue;
489
+ }
490
+ try {
491
+ const built = (0, bundle_1.buildBundle)({
492
+ root: gitState.sourceRoot,
493
+ profile: source.expected_profile,
494
+ output: outputPath,
495
+ });
496
+ sourceReceipt.new_bundle_hash = built.manifest.bundle_hash;
497
+ sourceReceipt.new_zip_sha256 = built.zipSha256;
498
+ planned.push({
499
+ source,
500
+ outputPath,
501
+ zip: built.zip,
502
+ newBundleHash: built.manifest.bundle_hash,
503
+ newZipSha256: built.zipSha256,
504
+ });
505
+ }
506
+ catch (err) {
507
+ sourceReceipt.errors.push(err instanceof Error ? err.message : String(err));
508
+ errors.push(`source ${source.path}: ${sourceReceipt.errors[sourceReceipt.errors.length - 1]}`);
509
+ }
510
+ }
511
+ if (errors.length > 0) {
512
+ return receipt;
513
+ }
514
+ if (options.dryRun) {
515
+ receipt.skipped = true;
516
+ return receipt;
517
+ }
518
+ for (const item of planned) {
519
+ const sourceReceipt = sources.find((source) => source.path === item.source.path);
520
+ try {
521
+ (0, atomic_1.atomicWriteFile)(item.outputPath, item.zip);
522
+ const verify = (0, bundle_1.verifyBundle)(gitState.sourceRoot, item.outputPath);
523
+ sourceReceipt.verified = verify.ok;
524
+ if (!verify.ok) {
525
+ sourceReceipt.errors.push(...verify.errors, ...verify.stale_paths.map((stale) => `stale: ${stale}`));
526
+ errors.push(`source ${item.source.path}: bundle verify failed`);
527
+ }
528
+ }
529
+ catch (err) {
530
+ const message = err instanceof Error ? err.message : String(err);
531
+ sourceReceipt.errors.push(message);
532
+ errors.push(`source ${item.source.path}: ${message}`);
533
+ }
534
+ }
535
+ if (errors.length === 0) {
536
+ const raw = options.rawSubgraphs[options.alias];
537
+ if (isObject(raw)) {
538
+ options.rawSubgraphs[options.alias] = { ...raw, source_repo: gitState.sourceRepo };
539
+ }
540
+ receipt.updated = true;
541
+ }
542
+ return receipt;
543
+ }
544
+ function runSubgraphSyncCommand(options) {
545
+ const dryRun = Boolean(options.dryRun);
546
+ const allowDirty = Boolean(options.allowDirty);
547
+ withSubgraphLock(options.root, () => {
548
+ const config = (0, config_1.loadConfig)(options.root);
549
+ const aliases = selectAliases(config, options.alias, options.all);
550
+ const { configPath, raw } = readRawConfig(options.root);
551
+ const rawSubgraphs = getSubgraphs(raw);
552
+ const results = aliases.map((alias) => syncOneAlias({
553
+ root: options.root,
554
+ alias,
555
+ subgraph: config.subgraphs[alias],
556
+ rawSubgraphs,
557
+ dryRun,
558
+ allowDirty,
559
+ }));
560
+ const ok = results.every((item) => item.errors.length === 0);
561
+ let indexPaths;
562
+ if (!dryRun && results.some((item) => item.updated)) {
563
+ raw.subgraphs = rawSubgraphs;
564
+ (0, config_1.validateConfigSchema)(raw);
565
+ writeRawConfig(configPath, raw);
566
+ indexPaths = (0, reindex_1.writeDerivedIndexes)(options.root, (0, config_1.loadConfig)(options.root)).paths;
567
+ }
568
+ const receipt = {
569
+ action: dryRun ? "sync_dry_run" : "synced",
570
+ ok,
571
+ count: results.length,
572
+ updated: results.filter((item) => item.updated).map((item) => item.alias),
573
+ skipped: results.filter((item) => item.skipped).map((item) => item.alias),
574
+ errors: results.flatMap((item) => item.errors.map((error) => `${item.alias}: ${error}`)),
575
+ warnings: results.flatMap((item) => item.warnings.map((warning) => `${item.alias}: ${warning}`)),
576
+ ...(indexPaths ? { paths: indexPaths } : {}),
577
+ subgraphs: results,
578
+ };
579
+ if (options.json) {
580
+ writeJson(receipt);
581
+ }
582
+ else {
583
+ console.log(`${dryRun ? "subgraph sync dry-run" : "subgraphs synced"}: ${results.length}`);
584
+ for (const item of results) {
585
+ const status = item.errors.length > 0 ? "error" : item.updated ? "updated" : item.skipped ? "skipped" : "ok";
586
+ console.log(`${item.alias} | ${status} | ${item.new_source_repo ?? item.old_source_repo ?? "unknown"}`);
587
+ }
588
+ }
589
+ if (!ok) {
590
+ throw new errors_1.ValidationError("subgraph sync failed");
591
+ }
592
+ });
593
+ }
594
+ function safeZipEntryPath(entryName) {
595
+ const normalized = entryName.replace(/\\/g, "/");
596
+ const parts = normalized.split("/");
597
+ if (path_1.default.isAbsolute(normalized) || parts.some((part) => part === "..") || parts.some((part) => part.length === 0)) {
598
+ throw new errors_1.ValidationError(`unsafe bundle entry path: ${entryName}`);
599
+ }
600
+ return normalized;
601
+ }
602
+ function writeMaterializeGitignore(targetRoot) {
603
+ const gitignorePath = path_1.default.join(targetRoot, ".gitignore");
604
+ const required = ["*", "!.gitignore"];
605
+ const existing = fs_1.default.existsSync(gitignorePath)
606
+ ? fs_1.default.readFileSync(gitignorePath, "utf8").split(/\r?\n/)
607
+ : [];
608
+ const lines = existing.filter((line) => line.length > 0);
609
+ for (const line of required) {
610
+ if (!lines.includes(line)) {
611
+ lines.push(line);
612
+ }
613
+ }
614
+ (0, atomic_1.atomicWriteFile)(gitignorePath, `${lines.join("\n")}\n`);
615
+ }
616
+ function materializeOneAlias(options) {
617
+ const enabled = enabledSources(options.subgraph);
618
+ const errors = [];
619
+ const warnings = [];
620
+ const outputDir = path_1.default.join(options.targetRoot, options.alias);
621
+ if (enabled.length !== 1) {
622
+ errors.push(`materialize requires exactly one enabled source for ${options.alias}; found ${enabled.length}`);
623
+ return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
624
+ }
625
+ const source = enabled[0];
626
+ const bundlePath = path_1.default.resolve(options.root, source.path);
627
+ if (!fs_1.default.existsSync(bundlePath)) {
628
+ errors.push(`bundle not found: ${source.path}`);
629
+ return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
630
+ }
631
+ if (fs_1.default.existsSync(outputDir)) {
632
+ const marker = path_1.default.join(outputDir, ".mdkg-materialized.json");
633
+ if (!options.clean) {
634
+ errors.push(`materialized directory already exists: ${relativeToRoot(options.root, outputDir)}; use --clean`);
635
+ return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
636
+ }
637
+ if (!fs_1.default.existsSync(marker)) {
638
+ errors.push(`refusing to clean non-materialized directory: ${relativeToRoot(options.root, outputDir)}`);
639
+ return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
640
+ }
641
+ }
642
+ const tempDir = path_1.default.join(options.targetRoot, `.${options.alias}.${process.pid}.${Date.now()}.tmp`);
643
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
644
+ try {
645
+ const parsed = (0, bundle_1.parseBundle)(bundlePath);
646
+ fs_1.default.mkdirSync(tempDir, { recursive: true });
647
+ for (const [entryName, data] of parsed.entries.entries()) {
648
+ const safeName = safeZipEntryPath(entryName);
649
+ const target = path_1.default.join(tempDir, safeName);
650
+ fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
651
+ fs_1.default.writeFileSync(target, data);
652
+ }
653
+ const marker = {
654
+ tool: "mdkg",
655
+ kind: "subgraph_materialization",
656
+ alias: options.alias,
657
+ bundle_path: source.path,
658
+ bundle_hash: parsed.manifest.bundle_hash,
659
+ zip_sha256: (0, bundle_1.sha256Buffer)(fs_1.default.readFileSync(bundlePath)),
660
+ profile: parsed.manifest.profile,
661
+ source_repo: options.subgraph.source_repo ?? parsed.manifest.source.repo,
662
+ source_git_head: parsed.manifest.source.git_head,
663
+ generated_at: new Date().toISOString(),
664
+ mdkg_version: readPackageVersion(),
665
+ };
666
+ fs_1.default.writeFileSync(path_1.default.join(tempDir, ".mdkg-materialized.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
667
+ if (fs_1.default.existsSync(outputDir)) {
668
+ fs_1.default.rmSync(outputDir, { recursive: true, force: true });
669
+ }
670
+ fs_1.default.mkdirSync(path_1.default.dirname(outputDir), { recursive: true });
671
+ fs_1.default.renameSync(tempDir, outputDir);
672
+ return {
673
+ alias: options.alias,
674
+ ok: true,
675
+ output_path: relativeToRoot(options.root, outputDir),
676
+ bundle_path: source.path,
677
+ bundle_hash: parsed.manifest.bundle_hash,
678
+ profile: parsed.manifest.profile,
679
+ warnings,
680
+ errors,
681
+ };
682
+ }
683
+ catch (err) {
684
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
685
+ errors.push(err instanceof Error ? err.message : String(err));
686
+ return { alias: options.alias, ok: false, output_path: relativeToRoot(options.root, outputDir), warnings, errors };
687
+ }
688
+ }
689
+ function runSubgraphMaterializeCommand(options) {
690
+ withSubgraphLock(options.root, () => {
691
+ const config = (0, config_1.loadConfig)(options.root);
692
+ const aliases = selectAliases(config, options.alias, options.all);
693
+ const targetRoot = path_1.default.resolve(options.root, normalizeContained(options.target, "--target"));
694
+ const results = aliases.map((alias) => materializeOneAlias({
695
+ root: options.root,
696
+ alias,
697
+ subgraph: config.subgraphs[alias],
698
+ targetRoot,
699
+ clean: Boolean(options.clean),
700
+ }));
701
+ if (options.gitignore) {
702
+ fs_1.default.mkdirSync(targetRoot, { recursive: true });
703
+ writeMaterializeGitignore(targetRoot);
704
+ }
705
+ const ok = results.every((item) => item.ok === true);
706
+ const receipt = {
707
+ action: "materialized",
708
+ ok,
709
+ count: results.length,
710
+ target: relativeToRoot(options.root, targetRoot),
711
+ results,
712
+ errors: results.flatMap((item) => (item.errors ?? []).map((error) => `${item.alias}: ${error}`)),
713
+ warnings: results.flatMap((item) => (item.warnings ?? []).map((warning) => `${item.alias}: ${warning}`)),
714
+ };
715
+ if (options.json) {
716
+ writeJson(receipt);
717
+ }
718
+ else {
719
+ console.log(`subgraphs materialized: ${results.length}`);
720
+ for (const item of results) {
721
+ console.log(`${item.alias} | ${item.ok ? "ok" : "error"} | ${item.output_path}`);
722
+ }
723
+ }
724
+ if (!ok) {
725
+ throw new errors_1.ValidationError("subgraph materialize failed");
726
+ }
727
+ });
728
+ }
@@ -11,6 +11,7 @@ const migrate_1 = require("../core/migrate");
11
11
  const config_1 = require("../core/config");
12
12
  const paths_1 = require("../core/paths");
13
13
  const version_1 = require("../core/version");
14
+ const project_db_1 = require("../core/project_db");
14
15
  const errors_1 = require("../util/errors");
15
16
  const init_manifest_1 = require("./init_manifest");
16
17
  const skill_support_1 = require("./skill_support");
@@ -18,7 +19,12 @@ const skill_mirror_1 = require("./skill_mirror");
18
19
  const DEFAULT_SEED_SUBDIR = path_1.default.resolve(__dirname, "..", "init");
19
20
  const PROTECTED_CORE_DOCS = new Set([".mdkg/core/SOUL.md", ".mdkg/core/HUMAN.md"]);
20
21
  const CREATE_ONLY_PRESERVED = new Set([".mdkg/core/core.md"]);
21
- const LOCAL_STATE_IGNORE_ENTRIES = [".mdkg/state/", ".mdkg/archive/**/source/"];
22
+ const LOCAL_STATE_IGNORE_ENTRIES = [
23
+ ".mdkg/state/",
24
+ ".mdkg/subgraphs/",
25
+ ".mdkg/archive/**/source/",
26
+ ...project_db_1.PROJECT_DB_GITIGNORE_ENTRIES,
27
+ ];
22
28
  function seededInitEvent(nowIso) {
23
29
  const event = {
24
30
  ts: nowIso,
@@ -215,23 +221,53 @@ function migrateLegacyBundleImportsConfig(input) {
215
221
  delete raw.bundle_imports;
216
222
  return { config: raw, changed: true };
217
223
  }
224
+ function migrateProjectDbConfig(input) {
225
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
226
+ return { config: input, changed: false };
227
+ }
228
+ const raw = { ...input };
229
+ if (raw.db !== undefined) {
230
+ return { config: raw, changed: false };
231
+ }
232
+ raw.db = {
233
+ enabled: false,
234
+ schema_version: project_db_1.PROJECT_DB_CONFIG_SCHEMA_VERSION,
235
+ root_path: project_db_1.PROJECT_DB_RELATIVE_DIR,
236
+ schema_path: project_db_1.PROJECT_DB_SCHEMA_DIR,
237
+ migrations_path: project_db_1.PROJECT_DB_MIGRATIONS_DIR,
238
+ runtime_path: project_db_1.PROJECT_DB_RUNTIME_FILE,
239
+ state_path: project_db_1.PROJECT_DB_STATE_FILE,
240
+ receipts_path: project_db_1.PROJECT_DB_RECEIPTS_DIR,
241
+ migration_table: project_db_1.PROJECT_DB_MIGRATION_TABLE,
242
+ };
243
+ return { config: raw, changed: true };
244
+ }
218
245
  function migrateConfigIfNeeded(root, dryRun, summary, changes) {
219
246
  const cfgPath = (0, paths_1.configPath)(root);
220
247
  const raw = JSON.parse(fs_1.default.readFileSync(cfgPath, "utf8"));
221
248
  const migrated = (0, migrate_1.migrateConfig)(raw);
222
- const nextConfig = migrateLegacyBundleImportsConfig(migrated.config);
249
+ const bundleConfig = migrateLegacyBundleImportsConfig(migrated.config);
250
+ const nextConfig = migrateProjectDbConfig(bundleConfig.config);
223
251
  (0, config_1.validateConfigSchema)(nextConfig.config);
224
- if (migrated.from === migrated.to && !nextConfig.changed) {
252
+ if (migrated.from === migrated.to && !bundleConfig.changed && !nextConfig.changed) {
225
253
  summary.unchanged += 1;
226
254
  return;
227
255
  }
256
+ const reasons = [];
257
+ if (bundleConfig.changed) {
258
+ reasons.push("config subgraphs migration");
259
+ }
260
+ if (nextConfig.changed) {
261
+ reasons.push("project db config defaults");
262
+ }
263
+ if (migrated.from !== migrated.to) {
264
+ reasons.push(`schema_version ${migrated.from} -> ${migrated.to}`);
265
+ }
228
266
  record(summary, changes, {
229
267
  path: ".mdkg/config.json",
230
268
  category: "config",
231
269
  action: "migrate",
232
- reason: nextConfig.changed
233
- ? `config subgraphs migration${migrated.from === migrated.to ? "" : ` and schema_version ${migrated.from} -> ${migrated.to}`}`
234
- : `config schema_version ${migrated.from} -> ${migrated.to}`,
270
+ reason: reasons.join(" and "),
235
271
  });
236
272
  if (!dryRun) {
237
273
  writeFile(cfgPath, `${JSON.stringify(nextConfig.config, null, 2)}\n`);
@@ -316,7 +352,7 @@ function ensureArchiveIgnorePolicy(root, dryRun, summary, changes) {
316
352
  path: ".gitignore",
317
353
  category: "ignore_policy",
318
354
  action: fs_1.default.existsSync(ignorePath) ? "update" : "create",
319
- reason: "ignore local mdkg state and raw archive source copies while keeping authored graph records commit-eligible",
355
+ reason: "ignore local mdkg state, project DB runtime files, and raw archive source copies while keeping authored graph records commit-eligible",
320
356
  });
321
357
  if (!dryRun) {
322
358
  const suffix = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";