mdkg 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +103 -15
  2. package/CLI_COMMAND_MATRIX.md +109 -2
  3. package/README.md +40 -2
  4. package/dist/cli.js +262 -2
  5. package/dist/command-contract.json +1902 -524
  6. package/dist/commands/archive.js +31 -14
  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 +624 -0
  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/argparse.js +5 -0
  69. package/dist/util/lock.js +10 -9
  70. package/dist/util/zip.js +163 -9
  71. 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");
@@ -173,10 +174,10 @@ function walkArchiveSidecars(root) {
173
174
  function stringAttribute(value) {
174
175
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
175
176
  }
176
- function writeArchiveSidecar(sidecarPath, frontmatter, body) {
177
+ function writeArchiveSidecar(root, sidecarPath, frontmatter, body) {
177
178
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter);
178
179
  const content = ["---", ...lines, "---", body.trimStart()].join("\n");
179
- (0, atomic_1.atomicWriteFile)(sidecarPath, content.endsWith("\n") ? content : `${content}\n`);
180
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: toPosixPath(path_1.default.relative(root, sidecarPath)) }, content.endsWith("\n") ? content : `${content}\n`);
180
181
  }
181
182
  function verifyArchiveSidecar(root, ws, sidecarPath) {
182
183
  const relativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
@@ -302,18 +303,28 @@ function runArchiveAddCommandLocked(options) {
302
303
  const visibility = (0, visibility_1.normalizeVisibility)(options.visibility);
303
304
  const today = (0, date_1.formatDate)(options.now ?? new Date());
304
305
  const archiveDir = path_1.default.resolve(options.root, workspace.path, workspace.mdkg_dir, "archive", id);
306
+ const archiveRelativeDir = (0, workspace_path_1.workspaceDocumentRelativePath)(workspace.path, workspace.mdkg_dir, "archive", id);
305
307
  const rawDir = path_1.default.join(archiveDir, "source");
306
308
  const rawPath = path_1.default.join(rawDir, basename);
307
309
  const zipPath = path_1.default.join(archiveDir, `${basename}.zip`);
308
310
  const sidecarPath = path_1.default.join(archiveDir, `${basename}.md`);
309
- if (fs_1.default.existsSync(sidecarPath)) {
311
+ const sidecarRelativePath = `${archiveRelativeDir}/${basename}.md`;
312
+ const rawRelativePath = `${archiveRelativeDir}/source/${basename}`;
313
+ const zipRelativePath = `${archiveRelativeDir}/${basename}.zip`;
314
+ if ((0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: sidecarRelativePath })) {
310
315
  throw new errors_1.UsageError(`archive sidecar already exists: ${path_1.default.relative(options.root, sidecarPath)}`);
311
316
  }
312
- fs_1.default.mkdirSync(rawDir, { recursive: true });
313
- fs_1.default.copyFileSync(sourcePath, rawPath);
314
- const rawData = fs_1.default.readFileSync(rawPath);
317
+ for (const [relativePath, operation] of [
318
+ [rawRelativePath, "create"],
319
+ [zipRelativePath, "replace"],
320
+ [sidecarRelativePath, "create"],
321
+ ]) {
322
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath, operation, createParents: true }, () => undefined);
323
+ }
324
+ const rawData = fs_1.default.readFileSync(sourcePath);
325
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: rawRelativePath }, rawData);
315
326
  const zipData = (0, zip_1.createDeterministicZip)(basename, rawData);
316
- (0, atomic_1.atomicWriteFile)(zipPath, zipData);
327
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: zipRelativePath }, zipData);
317
328
  const frontmatter = {
318
329
  id,
319
330
  type: "archive",
@@ -339,7 +350,7 @@ function runArchiveAddCommandLocked(options) {
339
350
  created: today,
340
351
  updated: today,
341
352
  };
342
- writeArchiveSidecar(sidecarPath, frontmatter, ["# Archive Entry", "", `Archived ${archiveKind}: ${basename}`, "", "# Provenance", "", "Copied from local workspace input."].join("\n"));
353
+ writeArchiveSidecar(options.root, sidecarPath, frontmatter, ["# Archive Entry", "", `Archived ${archiveKind}: ${basename}`, "", "# Provenance", "", "Copied from local workspace input."].join("\n"));
343
354
  maybeReindex(options.root);
344
355
  (0, event_support_1.appendAutomaticEvent)({
345
356
  root: options.root,
@@ -438,13 +449,19 @@ function runArchiveCompressCommandLocked(options) {
438
449
  const today = (0, date_1.formatDate)(options.now ?? new Date());
439
450
  for (const node of nodes) {
440
451
  const { sidecarPath, rawPath, zipPath } = archiveNodePaths(options.root, node);
441
- if (!fs_1.default.existsSync(rawPath)) {
452
+ const rawRelativePath = toPosixPath(path_1.default.relative(options.root, rawPath));
453
+ const zipRelativePath = toPosixPath(path_1.default.relative(options.root, zipPath));
454
+ const sidecarRelativePath = toPosixPath(path_1.default.relative(options.root, sidecarPath));
455
+ for (const relativePath of [rawRelativePath, zipRelativePath, sidecarRelativePath]) {
456
+ (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath, operation: relativePath === rawRelativePath ? "read" : "replace" }, () => undefined);
457
+ }
458
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: rawRelativePath })) {
442
459
  throw new errors_1.NotFoundError(`raw archive file missing for ${node.qid}: ${path_1.default.relative(options.root, rawPath)}`);
443
460
  }
444
- const rawData = fs_1.default.readFileSync(rawPath);
461
+ const rawData = (0, filesystem_authority_1.readContainedFile)({ root: options.root, relativePath: rawRelativePath }, null);
445
462
  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);
463
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: zipRelativePath }, zipData);
464
+ const parsed = (0, frontmatter_1.parseFrontmatter)((0, filesystem_authority_1.readContainedFile)({ root: options.root, relativePath: sidecarRelativePath }), sidecarPath);
448
465
  const nextFrontmatter = {
449
466
  ...parsed.frontmatter,
450
467
  byte_size: String(rawData.length),
@@ -453,7 +470,7 @@ function runArchiveCompressCommandLocked(options) {
453
470
  ingest_status: "compressed",
454
471
  updated: today,
455
472
  };
456
- writeArchiveSidecar(sidecarPath, nextFrontmatter, parsed.body);
473
+ writeArchiveSidecar(options.root, sidecarPath, nextFrontmatter, parsed.body);
457
474
  updated.push({
458
475
  workspace: node.ws,
459
476
  id: node.id,
@@ -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) : "";
@@ -12,9 +12,10 @@ exports.normalizeEventRefList = normalizeEventRefList;
12
12
  exports.normalizeEventStringList = normalizeEventStringList;
13
13
  exports.appendEvent = appendEvent;
14
14
  exports.appendAutomaticEvent = appendAutomaticEvent;
15
- const fs_1 = __importDefault(require("fs"));
16
15
  const path_1 = __importDefault(require("path"));
17
16
  const config_1 = require("../core/config");
17
+ const filesystem_authority_1 = require("../core/filesystem_authority");
18
+ const workspace_path_1 = require("../core/workspace_path");
18
19
  const errors_1 = require("../util/errors");
19
20
  function normalizeWorkspaceForEvents(config, raw) {
20
21
  const normalized = (raw ?? "root").toLowerCase();
@@ -33,6 +34,13 @@ function resolveEventsPath(root, config, ws = "root") {
33
34
  }
34
35
  return path_1.default.resolve(root, entry.path, entry.mdkg_dir, "work", "events", "events.jsonl");
35
36
  }
37
+ function resolveEventsRelativePath(config, ws) {
38
+ const entry = config.workspaces[ws];
39
+ if (!entry) {
40
+ throw new errors_1.NotFoundError(`workspace not found: ${ws}`);
41
+ }
42
+ return (0, workspace_path_1.workspaceDocumentRelativePath)(entry.path, entry.mdkg_dir, "work", "events", "events.jsonl");
43
+ }
36
44
  function formatRunIdTimestamp(now) {
37
45
  const iso = now.toISOString().replace(/[-:]/g, "").replace(/\./g, "").replace("Z", "Z");
38
46
  return iso;
@@ -43,16 +51,16 @@ function createLocalRunId(kind, now = new Date()) {
43
51
  }
44
52
  function isEventLoggingEnabled(root, config, ws) {
45
53
  const normalizedWs = normalizeWorkspaceForEvents(config, ws);
46
- return fs_1.default.existsSync(resolveEventsPath(root, config, normalizedWs));
54
+ return (0, filesystem_authority_1.containedPathExists)({ root, relativePath: resolveEventsRelativePath(config, normalizedWs) });
47
55
  }
48
56
  function ensureEventsEnabled(options) {
49
57
  const config = (0, config_1.loadConfig)(options.root);
50
58
  const ws = normalizeWorkspaceForEvents(config, options.ws);
51
59
  const eventsPath = resolveEventsPath(options.root, config, ws);
60
+ const relativePath = resolveEventsRelativePath(config, ws);
52
61
  let created = false;
53
- if (!fs_1.default.existsSync(eventsPath)) {
54
- fs_1.default.mkdirSync(path_1.default.dirname(eventsPath), { recursive: true });
55
- fs_1.default.writeFileSync(eventsPath, "", "utf8");
62
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath })) {
63
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath }, "");
56
64
  created = true;
57
65
  }
58
66
  return { ws, eventsPath, created };
@@ -102,10 +110,11 @@ function appendEvent(options) {
102
110
  throw new errors_1.UsageError("--refs requires at least one id or qid");
103
111
  }
104
112
  const eventsPath = resolveEventsPath(options.root, config, record.workspace);
105
- if (!fs_1.default.existsSync(eventsPath)) {
113
+ const relativePath = resolveEventsRelativePath(config, record.workspace);
114
+ if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath })) {
106
115
  throw new errors_1.NotFoundError(`events.jsonl is missing for workspace ${record.workspace}; run \`mdkg event enable --ws ${record.workspace}\``);
107
116
  }
108
- fs_1.default.appendFileSync(eventsPath, `${JSON.stringify(record)}\n`, "utf8");
117
+ (0, filesystem_authority_1.appendContainedFile)({ root: options.root, relativePath }, `${JSON.stringify(record)}\n`);
109
118
  return record;
110
119
  }
111
120
  function appendAutomaticEvent(options) {
@@ -13,6 +13,7 @@ const child_process_1 = require("child_process");
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const config_1 = require("../core/config");
16
+ const filesystem_authority_1 = require("../core/filesystem_authority");
16
17
  const capabilities_indexer_1 = require("../graph/capabilities_indexer");
17
18
  const capabilities_index_cache_1 = require("../graph/capabilities_index_cache");
18
19
  const staleness_1 = require("../graph/staleness");
@@ -26,7 +27,6 @@ const template_schema_1 = require("../graph/template_schema");
26
27
  const workspace_files_1 = require("../graph/workspace_files");
27
28
  const errors_1 = require("../util/errors");
28
29
  const refs_1 = require("../util/refs");
29
- const atomic_1 = require("../util/atomic");
30
30
  const lock_1 = require("../util/lock");
31
31
  const index_1 = require("./index");
32
32
  const FAMILY_VALUES = new Set(["index", "refs", "ids", "all"]);
@@ -1211,12 +1211,12 @@ function applyDuplicateIdChange(root, change) {
1211
1211
  if (!fromId || !toId) {
1212
1212
  throw new errors_1.UsageError(`fix apply change ${change.id} is missing duplicate id rewrite details`);
1213
1213
  }
1214
- const current = fs_1.default.readFileSync(absPath, "utf8");
1214
+ const current = (0, filesystem_authority_1.readContainedFile)({ root, relativePath });
1215
1215
  const rewritten = rewriteIdInNodeContent(current, fromId, toId);
1216
1216
  if (rewritten === current) {
1217
1217
  throw new errors_1.UsageError(`fix apply change ${change.id} produced no file changes`);
1218
1218
  }
1219
- (0, atomic_1.atomicWriteFile)(absPath, rewritten);
1219
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath }, rewritten);
1220
1220
  const afterDetails = change.after;
1221
1221
  const safeReferenceRewrites = Array.isArray(afterDetails.safe_reference_rewrites)
1222
1222
  ? afterDetails.safe_reference_rewrites
@@ -1227,13 +1227,13 @@ function applyDuplicateIdChange(root, change) {
1227
1227
  continue;
1228
1228
  }
1229
1229
  const rewriteAbs = path_1.default.resolve(root, rewrite.path);
1230
- if (!isInsideRoot(root, rewriteAbs) || !fs_1.default.existsSync(rewriteAbs)) {
1230
+ if (!isInsideRoot(root, rewriteAbs) || !(0, filesystem_authority_1.containedPathExists)({ root, relativePath: rewrite.path })) {
1231
1231
  continue;
1232
1232
  }
1233
- const rewriteCurrent = fs_1.default.readFileSync(rewriteAbs, "utf8");
1233
+ const rewriteCurrent = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: rewrite.path });
1234
1234
  const rewriteNext = rewriteCurrent.split(rewrite.from).join(rewrite.to);
1235
1235
  if (rewriteNext !== rewriteCurrent) {
1236
- (0, atomic_1.atomicWriteFile)(rewriteAbs, rewriteNext);
1236
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: rewrite.path }, rewriteNext);
1237
1237
  touchedPaths.add(rewrite.path);
1238
1238
  }
1239
1239
  }
@@ -1273,8 +1273,11 @@ function applyGitStageDuplicateIdChange(root, change) {
1273
1273
  throw new errors_1.UsageError(`fix apply refused path outside repo while resolving ${conflictPath}`);
1274
1274
  }
1275
1275
  const rewrittenDuplicate = rewriteIdInNodeContent(duplicateContent, fromId, candidateId);
1276
- (0, atomic_1.atomicWriteFile)(canonicalAbs, canonicalContent);
1277
- (0, atomic_1.atomicWriteFile)(candidateAbs, rewrittenDuplicate);
1276
+ for (const relativePath of [conflictPath, candidatePath]) {
1277
+ (0, filesystem_authority_1.withContainedPathSink)({ root, relativePath, operation: "replace", createParents: true }, () => undefined);
1278
+ }
1279
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: conflictPath }, canonicalContent);
1280
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: candidatePath }, rewrittenDuplicate);
1278
1281
  runGitStrict(root, ["add", "--", conflictPath, candidatePath]);
1279
1282
  return {
1280
1283
  id: change.id,