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
@@ -11,6 +11,8 @@ exports.runArchiveCompressCommand = runArchiveCompressCommand;
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const config_1 = require("../core/config");
14
+ const filesystem_authority_1 = require("../core/filesystem_authority");
15
+ const workspace_path_1 = require("../core/workspace_path");
14
16
  const frontmatter_1 = require("../graph/frontmatter");
15
17
  const archive_integrity_1 = require("../graph/archive_integrity");
16
18
  const indexer_1 = require("../graph/indexer");
@@ -21,7 +23,6 @@ const date_1 = require("../util/date");
21
23
  const errors_1 = require("../util/errors");
22
24
  const id_1 = require("../util/id");
23
25
  const refs_1 = require("../util/refs");
24
- const atomic_1 = require("../util/atomic");
25
26
  const lock_1 = require("../util/lock");
26
27
  const zip_1 = require("../util/zip");
27
28
  const event_support_1 = require("./event_support");
@@ -82,6 +83,22 @@ function normalizeArchiveId(raw) {
82
83
  }
83
84
  return normalized;
84
85
  }
86
+ function archiveTargetFromInput(value, ws) {
87
+ const uriId = (0, refs_1.archiveIdFromUri)(value);
88
+ if (uriId) {
89
+ return { id: normalizeArchiveId(uriId), workspace: normalizeWorkspace(ws) };
90
+ }
91
+ const separator = value.indexOf(":");
92
+ if (separator > 0) {
93
+ const workspace = normalizeWorkspace(value.slice(0, separator));
94
+ const id = normalizeArchiveId(value.slice(separator + 1));
95
+ if (ws && normalizeWorkspace(ws) !== workspace) {
96
+ throw new errors_1.UsageError(`archive qid ${value} conflicts with --ws ${normalizeWorkspace(ws)}`);
97
+ }
98
+ return { id, workspace };
99
+ }
100
+ return { id: normalizeArchiveId(value), workspace: normalizeWorkspace(ws) };
101
+ }
85
102
  function archiveIdFromInput(value) {
86
103
  return normalizeArchiveId((0, refs_1.archiveIdFromUri)(value) ?? value);
87
104
  }
@@ -132,9 +149,8 @@ function maybeReindex(root) {
132
149
  function resolveArchiveNode(root, id, ws) {
133
150
  const config = (0, config_1.loadConfig)(root);
134
151
  const { index } = (0, index_cache_1.loadIndex)({ root, config });
135
- const archiveId = archiveIdFromInput(id);
136
- const wsHint = normalizeWorkspace(ws);
137
- const qid = archiveId.includes(":") ? archiveId : `${wsHint}:${archiveId}`;
152
+ const target = archiveTargetFromInput(id, ws);
153
+ const qid = `${target.workspace}:${target.id}`;
138
154
  const node = index.nodes[qid];
139
155
  if (!node || node.type !== "archive") {
140
156
  throw new errors_1.NotFoundError(`archive not found: ${id}`);
@@ -142,6 +158,9 @@ function resolveArchiveNode(root, id, ws) {
142
158
  return node;
143
159
  }
144
160
  function archiveNodePaths(root, node) {
161
+ if (node.source?.imported || node.source?.read_only || node.path.includes("#")) {
162
+ throw new errors_1.ValidationError(`refusing to derive filesystem paths for read-only archive projection ${node.qid}`);
163
+ }
145
164
  const sidecarPath = path_1.default.resolve(root, node.path);
146
165
  const sidecarDir = path_1.default.dirname(sidecarPath);
147
166
  return {
@@ -173,10 +192,13 @@ function walkArchiveSidecars(root) {
173
192
  function stringAttribute(value) {
174
193
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
175
194
  }
176
- function writeArchiveSidecar(sidecarPath, frontmatter, body) {
195
+ function writeArchiveSidecar(root, sidecarPath, frontmatter, body) {
196
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: toPosixPath(path_1.default.relative(root, sidecarPath)) }, formatArchiveSidecar(frontmatter, body));
197
+ }
198
+ function formatArchiveSidecar(frontmatter, body) {
177
199
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter);
178
200
  const content = ["---", ...lines, "---", body.trimStart()].join("\n");
179
- (0, atomic_1.atomicWriteFile)(sidecarPath, content.endsWith("\n") ? content : `${content}\n`);
201
+ return content.endsWith("\n") ? content : `${content}\n`;
180
202
  }
181
203
  function verifyArchiveSidecar(root, ws, sidecarPath) {
182
204
  const relativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
@@ -302,18 +324,28 @@ function runArchiveAddCommandLocked(options) {
302
324
  const visibility = (0, visibility_1.normalizeVisibility)(options.visibility);
303
325
  const today = (0, date_1.formatDate)(options.now ?? new Date());
304
326
  const archiveDir = path_1.default.resolve(options.root, workspace.path, workspace.mdkg_dir, "archive", id);
327
+ const archiveRelativeDir = (0, workspace_path_1.workspaceDocumentRelativePath)(workspace.path, workspace.mdkg_dir, "archive", id);
305
328
  const rawDir = path_1.default.join(archiveDir, "source");
306
329
  const rawPath = path_1.default.join(rawDir, basename);
307
330
  const zipPath = path_1.default.join(archiveDir, `${basename}.zip`);
308
331
  const sidecarPath = path_1.default.join(archiveDir, `${basename}.md`);
309
- if (fs_1.default.existsSync(sidecarPath)) {
332
+ const sidecarRelativePath = `${archiveRelativeDir}/${basename}.md`;
333
+ const rawRelativePath = `${archiveRelativeDir}/source/${basename}`;
334
+ const zipRelativePath = `${archiveRelativeDir}/${basename}.zip`;
335
+ if ((0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: sidecarRelativePath })) {
310
336
  throw new errors_1.UsageError(`archive sidecar already exists: ${path_1.default.relative(options.root, sidecarPath)}`);
311
337
  }
312
- fs_1.default.mkdirSync(rawDir, { recursive: true });
313
- fs_1.default.copyFileSync(sourcePath, rawPath);
314
- const rawData = fs_1.default.readFileSync(rawPath);
338
+ for (const [relativePath, operation] of [
339
+ [rawRelativePath, "create"],
340
+ [zipRelativePath, "replace"],
341
+ [sidecarRelativePath, "create"],
342
+ ]) {
343
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath, operation, createParents: true }, () => undefined);
344
+ }
345
+ const rawData = fs_1.default.readFileSync(sourcePath);
346
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: rawRelativePath }, rawData);
315
347
  const zipData = (0, zip_1.createDeterministicZip)(basename, rawData);
316
- (0, atomic_1.atomicWriteFile)(zipPath, zipData);
348
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: zipRelativePath }, zipData);
317
349
  const frontmatter = {
318
350
  id,
319
351
  type: "archive",
@@ -339,7 +371,7 @@ function runArchiveAddCommandLocked(options) {
339
371
  created: today,
340
372
  updated: today,
341
373
  };
342
- writeArchiveSidecar(sidecarPath, frontmatter, ["# Archive Entry", "", `Archived ${archiveKind}: ${basename}`, "", "# Provenance", "", "Copied from local workspace input."].join("\n"));
374
+ writeArchiveSidecar(options.root, sidecarPath, frontmatter, ["# Archive Entry", "", `Archived ${archiveKind}: ${basename}`, "", "# Provenance", "", "Copied from local workspace input."].join("\n"));
343
375
  maybeReindex(options.root);
344
376
  (0, event_support_1.appendAutomaticEvent)({
345
377
  root: options.root,
@@ -425,36 +457,137 @@ function runArchiveVerifyCommand(options) {
425
457
  throw new errors_1.ValidationError("archive verification failed");
426
458
  }
427
459
  }
428
- function runArchiveCompressCommandLocked(options) {
429
- if (!options.all && !options.id) {
430
- throw new errors_1.UsageError("archive compress requires <id-or-archive-uri> or --all");
460
+ function isReadOnlyArchiveProjection(config, node) {
461
+ return Boolean(node.source?.imported ||
462
+ node.source?.read_only ||
463
+ config.subgraphs[node.source?.subgraph_alias ?? node.ws]);
464
+ }
465
+ function readOnlyArchiveError(node) {
466
+ const alias = node.source?.subgraph_alias ?? node.ws;
467
+ return new errors_1.ValidationError(`cannot compress read-only archive ${node.qid} from imported workspace ${alias}; ` +
468
+ "run compression in the source workspace and refresh the subgraph bundle");
469
+ }
470
+ function readOnlyWorkspaceError(alias) {
471
+ return new errors_1.ValidationError(`cannot compress archives in read-only imported workspace ${alias}; ` +
472
+ "run compression in the source workspace and refresh the subgraph bundle");
473
+ }
474
+ function assertWritableArchiveOwner(config, node) {
475
+ if (isReadOnlyArchiveProjection(config, node)) {
476
+ throw readOnlyArchiveError(node);
431
477
  }
432
- const config = (0, config_1.loadConfig)(options.root);
433
- const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
434
- const nodes = options.all
435
- ? Object.values(index.nodes).filter((node) => node.type === "archive")
436
- : [resolveArchiveNode(options.root, String(options.id), options.ws)];
437
- const updated = [];
438
- const today = (0, date_1.formatDate)(options.now ?? new Date());
439
- for (const node of nodes) {
440
- const { sidecarPath, rawPath, zipPath } = archiveNodePaths(options.root, node);
441
- if (!fs_1.default.existsSync(rawPath)) {
442
- throw new errors_1.NotFoundError(`raw archive file missing for ${node.qid}: ${path_1.default.relative(options.root, rawPath)}`);
478
+ const workspace = config.workspaces[node.ws];
479
+ if (!workspace) {
480
+ throw new errors_1.ValidationError(`archive ${node.qid} has no configured local workspace owner`);
481
+ }
482
+ if (!workspace.enabled) {
483
+ throw new errors_1.ValidationError(`archive workspace ${node.ws} is disabled and not writable`);
484
+ }
485
+ const archiveRoot = (0, workspace_path_1.workspaceDocumentRelativePath)(workspace.path, workspace.mdkg_dir, "archive");
486
+ const nodePath = toPosixPath(node.path);
487
+ if (!nodePath.startsWith(`${archiveRoot}/`)) {
488
+ throw new errors_1.ValidationError(`archive ${node.qid} path is outside its configured local archive root: ${archiveRoot}`);
489
+ }
490
+ }
491
+ function resolveArchiveNodeFromIndex(index, id, ws) {
492
+ const target = archiveTargetFromInput(id, ws);
493
+ const qid = `${target.workspace}:${target.id}`;
494
+ const node = index.nodes[qid];
495
+ if (!node || node.type !== "archive") {
496
+ throw new errors_1.NotFoundError(`archive not found: ${id}`);
497
+ }
498
+ return node;
499
+ }
500
+ function selectArchiveCompressionNodes(options, config, index) {
501
+ if (options.all && options.id) {
502
+ throw new errors_1.UsageError("archive compress accepts either <id-or-archive-uri-or-qid> or --all, not both");
503
+ }
504
+ const requestedWorkspace = options.ws ? normalizeWorkspace(options.ws) : null;
505
+ const excluded = [];
506
+ let nodes;
507
+ if (options.all) {
508
+ if (requestedWorkspace && config.subgraphs[requestedWorkspace]) {
509
+ throw readOnlyWorkspaceError(requestedWorkspace);
443
510
  }
444
- const rawData = fs_1.default.readFileSync(rawPath);
445
- const zipData = (0, zip_1.createDeterministicZip)(path_1.default.basename(rawPath), rawData);
446
- (0, atomic_1.atomicWriteFile)(zipPath, zipData);
447
- const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(sidecarPath, "utf8"), sidecarPath);
448
- const nextFrontmatter = {
449
- ...parsed.frontmatter,
450
- byte_size: String(rawData.length),
451
- sha256: (0, archive_integrity_1.hashArchiveBuffer)(rawData),
452
- compressed_sha256: (0, archive_integrity_1.hashArchiveBuffer)(zipData),
453
- ingest_status: "compressed",
454
- updated: today,
455
- };
456
- writeArchiveSidecar(sidecarPath, nextFrontmatter, parsed.body);
457
- updated.push({
511
+ if (requestedWorkspace) {
512
+ const workspace = config.workspaces[requestedWorkspace];
513
+ if (!workspace) {
514
+ throw new errors_1.NotFoundError(`workspace not found: ${requestedWorkspace}`);
515
+ }
516
+ if (!workspace.enabled) {
517
+ throw new errors_1.ValidationError(`archive workspace ${requestedWorkspace} is disabled and not writable`);
518
+ }
519
+ }
520
+ nodes = Object.values(index.nodes)
521
+ .filter((node) => node.type === "archive")
522
+ .sort((a, b) => a.qid.localeCompare(b.qid))
523
+ .filter((node) => {
524
+ if (requestedWorkspace && node.ws !== requestedWorkspace) {
525
+ return false;
526
+ }
527
+ if (isReadOnlyArchiveProjection(config, node)) {
528
+ if (!requestedWorkspace) {
529
+ excluded.push({
530
+ workspace: node.ws,
531
+ qid: node.qid,
532
+ reason: "read_only_imported_subgraph",
533
+ });
534
+ }
535
+ return false;
536
+ }
537
+ assertWritableArchiveOwner(config, node);
538
+ return true;
539
+ });
540
+ }
541
+ else {
542
+ const node = resolveArchiveNodeFromIndex(index, String(options.id), options.ws);
543
+ assertWritableArchiveOwner(config, node);
544
+ nodes = [node];
545
+ }
546
+ return {
547
+ nodes,
548
+ selection: {
549
+ requested_workspace: requestedWorkspace,
550
+ selected_workspaces: Array.from(new Set(nodes.map((node) => node.ws))).sort(),
551
+ excluded_read_only: excluded.sort((a, b) => a.qid.localeCompare(b.qid)),
552
+ },
553
+ };
554
+ }
555
+ function preflightArchiveCompression(root, node, today) {
556
+ for (const attribute of ["stored_path", "compressed_path"]) {
557
+ if (typeof node.attributes[attribute] !== "string" || !String(node.attributes[attribute]).trim()) {
558
+ throw new errors_1.ValidationError(`${node.qid}: ${attribute} is required before archive compression`);
559
+ }
560
+ }
561
+ const { sidecarPath, rawPath, zipPath } = archiveNodePaths(root, node);
562
+ const rawRelativePath = toPosixPath(path_1.default.relative(root, rawPath));
563
+ const zipRelativePath = toPosixPath(path_1.default.relative(root, zipPath));
564
+ const sidecarRelativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
565
+ for (const relativePath of [rawRelativePath, zipRelativePath, sidecarRelativePath]) {
566
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath, operation: relativePath === rawRelativePath ? "read" : "replace" }, () => undefined);
567
+ }
568
+ if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: rawRelativePath })) {
569
+ throw new errors_1.NotFoundError(`raw archive file missing for ${node.qid}: ${path_1.default.relative(root, rawPath)}`);
570
+ }
571
+ const rawData = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: rawRelativePath }, null);
572
+ const parsed = (0, frontmatter_1.parseFrontmatter)((0, filesystem_authority_1.readContainedFile)({ root, relativePath: sidecarRelativePath }), sidecarPath);
573
+ if (parsed.frontmatter.type !== "archive" || parsed.frontmatter.id !== node.id) {
574
+ throw new errors_1.ValidationError(`${node.qid}: archive sidecar identity changed before compression`);
575
+ }
576
+ const zipData = (0, zip_1.createDeterministicZip)(path_1.default.basename(rawPath), rawData);
577
+ const nextFrontmatter = {
578
+ ...parsed.frontmatter,
579
+ byte_size: String(rawData.length),
580
+ sha256: (0, archive_integrity_1.hashArchiveBuffer)(rawData),
581
+ compressed_sha256: (0, archive_integrity_1.hashArchiveBuffer)(zipData),
582
+ ingest_status: "compressed",
583
+ updated: today,
584
+ };
585
+ return {
586
+ zipRelativePath,
587
+ sidecarRelativePath,
588
+ zipData,
589
+ sidecarContent: formatArchiveSidecar(nextFrontmatter, parsed.body),
590
+ receipt: {
458
591
  workspace: node.ws,
459
592
  id: node.id,
460
593
  qid: node.qid,
@@ -465,14 +598,35 @@ function runArchiveCompressCommandLocked(options) {
465
598
  sha256: String(nextFrontmatter.sha256),
466
599
  compressed_sha256: String(nextFrontmatter.compressed_sha256),
467
600
  visibility: (0, visibility_1.normalizeVisibility)(typeof nextFrontmatter.visibility === "string" ? nextFrontmatter.visibility : undefined, "archive visibility"),
468
- });
601
+ },
602
+ };
603
+ }
604
+ function runArchiveCompressCommandLocked(options) {
605
+ if (!options.all && !options.id) {
606
+ throw new errors_1.UsageError("archive compress requires <id-or-archive-uri-or-qid> or --all");
607
+ }
608
+ const config = (0, config_1.loadConfig)(options.root);
609
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
610
+ const { nodes, selection } = selectArchiveCompressionNodes(options, config, index);
611
+ const today = (0, date_1.formatDate)(options.now ?? new Date());
612
+ const plans = nodes.map((node) => preflightArchiveCompression(options.root, node, today));
613
+ const updated = [];
614
+ for (const plan of plans) {
615
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: plan.zipRelativePath }, plan.zipData);
616
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: plan.sidecarRelativePath }, plan.sidecarContent);
617
+ updated.push(plan.receipt);
469
618
  }
470
619
  maybeReindex(options.root);
471
620
  if (options.json) {
472
- console.log(JSON.stringify({ action: "compressed", count: updated.length, archives: updated }, null, 2));
621
+ console.log(JSON.stringify({ action: "compressed", count: updated.length, archives: updated, selection }, null, 2));
473
622
  return;
474
623
  }
475
624
  console.log(`archive compressed: ${updated.length}`);
625
+ console.log(`selected workspaces: ${selection.selected_workspaces.join(", ") || "none"}`);
626
+ console.log(`excluded read-only projections: ${selection.excluded_read_only.length}`);
627
+ for (const excluded of selection.excluded_read_only) {
628
+ console.log(` - ${excluded.qid} (${excluded.reason})`);
629
+ }
476
630
  }
477
631
  function withArchiveLock(root, fn) {
478
632
  const config = (0, config_1.loadConfig)(root);
@@ -3,6 +3,8 @@ 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.validateBundleManifest = validateBundleManifest;
7
+ exports.bundlePayloadErrors = bundlePayloadErrors;
6
8
  exports.sha256Buffer = sha256Buffer;
7
9
  exports.buildBundle = buildBundle;
8
10
  exports.parseBundle = parseBundle;
@@ -16,6 +18,8 @@ const fs_1 = __importDefault(require("fs"));
16
18
  const path_1 = __importDefault(require("path"));
17
19
  const child_process_1 = require("child_process");
18
20
  const config_1 = require("../core/config");
21
+ const filesystem_authority_1 = require("../core/filesystem_authority");
22
+ const atomic_1 = require("../util/atomic");
19
23
  const capabilities_indexer_1 = require("../graph/capabilities_indexer");
20
24
  const subgraphs_1 = require("../graph/subgraphs");
21
25
  const indexer_1 = require("../graph/indexer");
@@ -31,6 +35,168 @@ const INDEX_ENTRY_PATHS = {
31
35
  skills: ".mdkg/index/skills.json",
32
36
  capabilities: ".mdkg/index/capabilities.json",
33
37
  };
38
+ const REQUIRED_INDEX_PATHS = Object.values(INDEX_ENTRY_PATHS);
39
+ const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
40
+ function isRecord(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ function manifestError(message) {
44
+ throw new errors_1.ValidationError(`bundle manifest invalid: ${message}`);
45
+ }
46
+ function manifestString(value, field) {
47
+ if (typeof value !== "string" || value.length === 0) {
48
+ manifestError(`${field} must be a non-empty string`);
49
+ }
50
+ return value;
51
+ }
52
+ function manifestHash(value, field) {
53
+ const hash = manifestString(value, field);
54
+ if (!SHA256_PATTERN.test(hash)) {
55
+ manifestError(`${field} must be a sha256 digest`);
56
+ }
57
+ return hash;
58
+ }
59
+ function safeManifestPath(value, field) {
60
+ const candidate = manifestString(value, field).replace(/\\/g, "/");
61
+ const parts = candidate.split("/");
62
+ if (path_1.default.posix.isAbsolute(candidate) || parts.some((part) => part === "" || part === "." || part === "..")) {
63
+ manifestError(`${field} must be a safe relative path`);
64
+ }
65
+ return candidate;
66
+ }
67
+ function validateBundleManifest(value) {
68
+ if (!isRecord(value))
69
+ manifestError("root must be an object");
70
+ if (value.manifest_version !== 1)
71
+ manifestError("manifest_version must be 1");
72
+ if (value.tool !== "mdkg")
73
+ manifestError("tool must be mdkg");
74
+ const mdkgVersion = manifestString(value.mdkg_version, "mdkg_version");
75
+ if (value.profile !== "private" && value.profile !== "public") {
76
+ manifestError("profile must be private or public");
77
+ }
78
+ if (!Array.isArray(value.selected_workspaces) || value.selected_workspaces.length === 0) {
79
+ manifestError("selected_workspaces must be a non-empty string array");
80
+ }
81
+ const selectedWorkspaces = value.selected_workspaces.map((item, index) => manifestString(item, `selected_workspaces[${index}]`));
82
+ if (new Set(selectedWorkspaces).size !== selectedWorkspaces.length) {
83
+ manifestError("selected_workspaces must not contain duplicates");
84
+ }
85
+ if (!isRecord(value.source))
86
+ manifestError("source must be an object");
87
+ const repo = manifestString(value.source.repo, "source.repo");
88
+ const gitHead = value.source.git_head;
89
+ if (gitHead !== null && (typeof gitHead !== "string" || !/^[a-f0-9]{40}$/.test(gitHead))) {
90
+ manifestError("source.git_head must be null or a 40-character lowercase Git SHA");
91
+ }
92
+ if (typeof value.source.dirty !== "boolean")
93
+ manifestError("source.dirty must be boolean");
94
+ const sourceTreeHash = manifestHash(value.source_tree_hash, "source_tree_hash");
95
+ const bundleHash = manifestHash(value.bundle_hash, "bundle_hash");
96
+ if (!Number.isSafeInteger(value.file_count) || Number(value.file_count) < 0) {
97
+ manifestError("file_count must be a non-negative safe integer");
98
+ }
99
+ if (!isRecord(value.index_hashes))
100
+ manifestError("index_hashes must be an object");
101
+ const indexHashes = {};
102
+ for (const [rawPath, rawHash] of Object.entries(value.index_hashes)) {
103
+ const indexPath = safeManifestPath(rawPath, "index_hashes key");
104
+ indexHashes[indexPath] = manifestHash(rawHash, `index_hashes.${indexPath}`);
105
+ }
106
+ for (const requiredPath of REQUIRED_INDEX_PATHS) {
107
+ if (!indexHashes[requiredPath])
108
+ manifestError(`index_hashes missing required index ${requiredPath}`);
109
+ }
110
+ if (!Array.isArray(value.files))
111
+ manifestError("files must be an array");
112
+ const seenPaths = new Set();
113
+ const files = value.files.map((rawFile, index) => {
114
+ if (!isRecord(rawFile))
115
+ manifestError(`files[${index}] must be an object`);
116
+ const filePath = safeManifestPath(rawFile.path, `files[${index}].path`);
117
+ if (seenPaths.has(filePath))
118
+ manifestError(`duplicate file path: ${filePath}`);
119
+ seenPaths.add(filePath);
120
+ if (rawFile.kind !== "authored" && rawFile.kind !== "archive_cache" && rawFile.kind !== "generated_index") {
121
+ manifestError(`files[${index}].kind is invalid`);
122
+ }
123
+ if (!Number.isSafeInteger(rawFile.size) || Number(rawFile.size) < 0) {
124
+ manifestError(`files[${index}].size must be a non-negative safe integer`);
125
+ }
126
+ const workspace = rawFile.workspace === undefined
127
+ ? undefined
128
+ : manifestString(rawFile.workspace, `files[${index}].workspace`);
129
+ const visibility = rawFile.visibility;
130
+ if (visibility !== undefined && visibility !== "private" && visibility !== "internal" && visibility !== "public") {
131
+ manifestError(`files[${index}].visibility is invalid`);
132
+ }
133
+ return {
134
+ path: filePath,
135
+ kind: rawFile.kind,
136
+ ...(workspace ? { workspace } : {}),
137
+ ...(visibility ? { visibility } : {}),
138
+ size: Number(rawFile.size),
139
+ sha256: manifestHash(rawFile.sha256, `files[${index}].sha256`),
140
+ };
141
+ });
142
+ if (Number(value.file_count) !== files.length)
143
+ manifestError("file_count does not match files length");
144
+ for (const requiredPath of REQUIRED_INDEX_PATHS) {
145
+ const row = files.find((file) => file.path === requiredPath);
146
+ if (!row || row.kind !== "generated_index") {
147
+ manifestError(`files missing required generated index ${requiredPath}`);
148
+ }
149
+ if (row.sha256 !== indexHashes[requiredPath]) {
150
+ manifestError(`index hash does not match file row for ${requiredPath}`);
151
+ }
152
+ }
153
+ return {
154
+ manifest_version: 1,
155
+ tool: "mdkg",
156
+ mdkg_version: mdkgVersion,
157
+ profile: value.profile,
158
+ selected_workspaces: selectedWorkspaces,
159
+ source: { repo, git_head: gitHead, dirty: value.source.dirty },
160
+ source_tree_hash: sourceTreeHash,
161
+ bundle_hash: bundleHash,
162
+ file_count: files.length,
163
+ index_hashes: indexHashes,
164
+ files,
165
+ };
166
+ }
167
+ function bundlePayloadErrors(entries, manifest) {
168
+ const errors = [];
169
+ const manifestPaths = new Set(manifest.files.map((file) => file.path));
170
+ for (const entryPath of entries.keys()) {
171
+ if (entryPath !== MANIFEST_ENTRY && !manifestPaths.has(entryPath))
172
+ errors.push(`unexpected bundle entry: ${entryPath}`);
173
+ }
174
+ for (const file of manifest.files) {
175
+ const data = entries.get(file.path);
176
+ if (!data) {
177
+ errors.push(`missing bundle entry: ${file.path}`);
178
+ continue;
179
+ }
180
+ if (sha256Buffer(data) !== file.sha256)
181
+ errors.push(`hash mismatch for ${file.path}`);
182
+ if (data.length !== file.size)
183
+ errors.push(`size mismatch for ${file.path}`);
184
+ }
185
+ for (const requiredPath of REQUIRED_INDEX_PATHS) {
186
+ const data = entries.get(requiredPath);
187
+ if (!data)
188
+ continue;
189
+ if (sha256Buffer(data) !== manifest.index_hashes[requiredPath]) {
190
+ errors.push(`generated index hash mismatch: ${requiredPath}`);
191
+ }
192
+ }
193
+ if (hashManifestFiles(manifest.files.filter((file) => file.kind !== "generated_index")) !== manifest.source_tree_hash) {
194
+ errors.push("source_tree_hash mismatch");
195
+ }
196
+ if (hashManifestFiles(manifest.files) !== manifest.bundle_hash)
197
+ errors.push("bundle_hash mismatch");
198
+ return Array.from(new Set(errors));
199
+ }
34
200
  function toPosixPath(value) {
35
201
  return value.split(path_1.default.sep).join("/");
36
202
  }
@@ -519,9 +685,8 @@ function buildBundle(options) {
519
685
  };
520
686
  }
521
687
  function parseBundle(bundlePath) {
522
- const zip = fs_1.default.readFileSync(bundlePath);
523
688
  const entries = new Map();
524
- for (const entry of (0, zip_1.readZipEntries)(zip)) {
689
+ for (const entry of (0, zip_1.readZipFileEntries)(bundlePath)) {
525
690
  entries.set(entry.name, entry.data);
526
691
  }
527
692
  const manifestData = entries.get(MANIFEST_ENTRY);
@@ -529,10 +694,7 @@ function parseBundle(bundlePath) {
529
694
  throw new errors_1.ValidationError("bundle manifest missing");
530
695
  }
531
696
  try {
532
- return {
533
- entries,
534
- manifest: JSON.parse(manifestData.toString("utf8")),
535
- };
697
+ return { entries, manifest: validateBundleManifest(JSON.parse(manifestData.toString("utf8"))) };
536
698
  }
537
699
  catch (err) {
538
700
  const message = err instanceof Error ? err.message : String(err);
@@ -562,25 +724,8 @@ function verifyBundle(root, bundlePath) {
562
724
  stale_paths: [],
563
725
  };
564
726
  }
565
- const manifestPaths = new Set(manifest.files.map((file) => file.path));
566
- for (const entryPath of entries.keys()) {
567
- if (entryPath !== MANIFEST_ENTRY && !manifestPaths.has(entryPath)) {
568
- errors.push(`unexpected bundle entry: ${entryPath}`);
569
- }
570
- }
727
+ errors.push(...bundlePayloadErrors(entries, manifest));
571
728
  for (const file of manifest.files) {
572
- const data = entries.get(file.path);
573
- if (!data) {
574
- errors.push(`missing bundle entry: ${file.path}`);
575
- continue;
576
- }
577
- const hash = sha256Buffer(data);
578
- if (hash !== file.sha256) {
579
- errors.push(`hash mismatch for ${file.path}`);
580
- }
581
- if (data.length !== file.size) {
582
- errors.push(`size mismatch for ${file.path}`);
583
- }
584
729
  if (file.kind !== "generated_index") {
585
730
  const sourcePath = path_1.default.resolve(root, file.path);
586
731
  if (!fs_1.default.existsSync(sourcePath)) {
@@ -592,24 +737,6 @@ function verifyBundle(root, bundlePath) {
592
737
  }
593
738
  }
594
739
  }
595
- for (const [indexPath, expectedHash] of Object.entries(manifest.index_hashes)) {
596
- const data = entries.get(indexPath);
597
- if (!data) {
598
- errors.push(`missing generated index: ${indexPath}`);
599
- continue;
600
- }
601
- if (sha256Buffer(data) !== expectedHash) {
602
- errors.push(`generated index hash mismatch: ${indexPath}`);
603
- }
604
- }
605
- const computedSourceHash = hashManifestFiles(manifest.files.filter((file) => file.kind !== "generated_index"));
606
- if (computedSourceHash !== manifest.source_tree_hash) {
607
- errors.push("source_tree_hash mismatch");
608
- }
609
- const computedBundleHash = hashManifestFiles(manifest.files);
610
- if (computedBundleHash !== manifest.bundle_hash) {
611
- errors.push("bundle_hash mismatch");
612
- }
613
740
  const currentHead = gitOutput(root, ["rev-parse", "HEAD"]);
614
741
  if (manifest.source.git_head && currentHead && manifest.source.git_head !== currentHead) {
615
742
  stalePaths.push("git:HEAD");
@@ -645,8 +772,17 @@ function bundleSummary(manifest, bundlePath, zipSha256) {
645
772
  }
646
773
  function runBundleCreateCommand(options) {
647
774
  const result = buildBundle(options);
648
- fs_1.default.mkdirSync(path_1.default.dirname(result.outputPath), { recursive: true });
649
- fs_1.default.writeFileSync(result.outputPath, result.zip);
775
+ if (options.output && path_1.default.isAbsolute(options.output)) {
776
+ const external = (0, filesystem_authority_1.authorizeOperatorSelectedExternalPath)({
777
+ operation: "replace",
778
+ path: options.output,
779
+ operatorSelected: true,
780
+ });
781
+ (0, atomic_1.atomicWriteFile)(external.absolutePath, result.zip);
782
+ }
783
+ else {
784
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: path_1.default.relative(options.root, result.outputPath).split(path_1.default.sep).join("/") }, result.zip);
785
+ }
650
786
  const receipt = {
651
787
  action: "created",
652
788
  ...bundleSummary(result.manifest, path_1.default.relative(options.root, result.outputPath), result.zipSha256),
@@ -39,6 +39,7 @@ function loadCapabilityRecords(options) {
39
39
  config,
40
40
  useCache: !options.noCache,
41
41
  allowReindex: !options.noReindex,
42
+ persistReindex: false,
42
43
  });
43
44
  if (stale && !rebuilt && !options.noCache) {
44
45
  console.error("warning: capabilities index is stale; run mdkg index to refresh");
@@ -6,15 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CHECKPOINT_KINDS = void 0;
7
7
  exports.createCheckpoint = createCheckpoint;
8
8
  exports.runCheckpointNewCommand = runCheckpointNewCommand;
9
- const fs_1 = __importDefault(require("fs"));
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const config_1 = require("../core/config");
11
+ const filesystem_authority_1 = require("../core/filesystem_authority");
12
+ const workspace_path_1 = require("../core/workspace_path");
12
13
  const index_cache_1 = require("../graph/index_cache");
13
14
  const loader_1 = require("../templates/loader");
14
15
  const date_1 = require("../util/date");
15
16
  const errors_1 = require("../util/errors");
16
17
  const id_1 = require("../util/id");
17
- const atomic_1 = require("../util/atomic");
18
18
  const lock_1 = require("../util/lock");
19
19
  const sqlite_index_1 = require("../graph/sqlite_index");
20
20
  const event_support_1 = require("./event_support");
@@ -253,7 +253,8 @@ function createCheckpointLocked(options) {
253
253
  const wsEntry = config.workspaces[ws];
254
254
  const workDir = path_1.default.resolve(options.root, wsEntry.path, wsEntry.mdkg_dir, "work");
255
255
  const filePath = path_1.default.join(workDir, fileName);
256
- if (fs_1.default.existsSync(filePath)) {
256
+ const relativeFilePath = (0, workspace_path_1.workspaceDocumentRelativePath)(wsEntry.path, wsEntry.mdkg_dir, "work", fileName);
257
+ if ((0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: relativeFilePath })) {
257
258
  throw new errors_1.UsageError(`checkpoint file already exists: ${path_1.default.relative(options.root, filePath)}`);
258
259
  }
259
260
  const relates = parseCsvList(options.relates).map((value) => normalizeIdRef(value, "--relates"));
@@ -279,9 +280,9 @@ function createCheckpointLocked(options) {
279
280
  relates,
280
281
  scope,
281
282
  });
282
- const rendered = replaceRenderedBody(content, checkpointBody(kind));
283
+ const rendered = replaceRenderedBody(content, options.body ?? checkpointBody(kind));
283
284
  try {
284
- (0, atomic_1.writeFileExclusive)(filePath, rendered);
285
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: relativeFilePath }, rendered);
285
286
  }
286
287
  catch (err) {
287
288
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";