mdkg 0.3.4 → 0.3.5

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.
@@ -0,0 +1,582 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runGraphCloneCommand = runGraphCloneCommand;
7
+ exports.runGraphForkCommand = runGraphForkCommand;
8
+ exports.runGraphImportTemplateCommand = runGraphImportTemplateCommand;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const bundle_1 = require("./bundle");
12
+ const index_1 = require("./index");
13
+ const validate_1 = require("./validate");
14
+ const config_1 = require("../core/config");
15
+ const workspace_path_1 = require("../core/workspace_path");
16
+ const indexer_1 = require("../graph/indexer");
17
+ const frontmatter_1 = require("../graph/frontmatter");
18
+ const errors_1 = require("../util/errors");
19
+ const qid_1 = require("../util/qid");
20
+ const atomic_1 = require("../util/atomic");
21
+ const zip_1 = require("../util/zip");
22
+ const lock_1 = require("../util/lock");
23
+ function writeJson(value) {
24
+ console.log(JSON.stringify(value, null, 2));
25
+ }
26
+ function toPosixPath(value) {
27
+ return value.split(path_1.default.sep).join("/");
28
+ }
29
+ function escapeRegExp(value) {
30
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ }
32
+ function rel(root, target) {
33
+ return toPosixPath(path_1.default.relative(root, target)) || ".";
34
+ }
35
+ function isInside(parent, child) {
36
+ const relative = path_1.default.relative(parent, child);
37
+ return relative === "" || (!relative.startsWith("..") && !path_1.default.isAbsolute(relative));
38
+ }
39
+ function resolveSourcePath(root, source) {
40
+ if (source.includes("\0")) {
41
+ throw new errors_1.UsageError("source cannot contain NUL bytes");
42
+ }
43
+ return path_1.default.isAbsolute(source) ? source : path_1.default.resolve(root, source);
44
+ }
45
+ function safeZipEntryPath(entryName) {
46
+ const normalized = entryName.replace(/\\/g, "/");
47
+ const parts = normalized.split("/");
48
+ if (path_1.default.isAbsolute(normalized) ||
49
+ parts.some((part) => part === "..") ||
50
+ parts.some((part) => part.length === 0)) {
51
+ throw new errors_1.ValidationError(`unsafe graph source entry path: ${entryName}`);
52
+ }
53
+ return normalized;
54
+ }
55
+ function resolveSourceDirectory(sourcePath) {
56
+ if (!fs_1.default.existsSync(sourcePath) || !fs_1.default.statSync(sourcePath).isDirectory()) {
57
+ return undefined;
58
+ }
59
+ const directConfig = path_1.default.join(sourcePath, ".mdkg", "config.json");
60
+ if (fs_1.default.existsSync(directConfig)) {
61
+ return sourcePath;
62
+ }
63
+ if (path_1.default.basename(sourcePath) === ".mdkg" && fs_1.default.existsSync(path_1.default.join(sourcePath, "config.json"))) {
64
+ return path_1.default.dirname(sourcePath);
65
+ }
66
+ throw new errors_1.UsageError("directory source must contain .mdkg/config.json or be an .mdkg directory");
67
+ }
68
+ function loadGraphSource(root, source) {
69
+ const sourcePath = resolveSourcePath(root, source);
70
+ const sourceRoot = resolveSourceDirectory(sourcePath);
71
+ if (sourceRoot) {
72
+ const bundle = (0, bundle_1.buildBundle)({ root: sourceRoot, profile: "private" });
73
+ return {
74
+ kind: "directory",
75
+ sourcePath,
76
+ sourceRoot,
77
+ entries: new Map((0, zip_1.readZipEntries)(bundle.zip).map((entry) => [entry.name, entry.data])),
78
+ manifest: bundle.manifest,
79
+ zipSha256: bundle.zipSha256,
80
+ };
81
+ }
82
+ if (!fs_1.default.existsSync(sourcePath)) {
83
+ throw new errors_1.NotFoundError(`graph source not found: ${source}`);
84
+ }
85
+ if (!fs_1.default.statSync(sourcePath).isFile()) {
86
+ throw new errors_1.UsageError("source must be a bundle file or directory containing .mdkg");
87
+ }
88
+ const parsed = (0, bundle_1.parseBundle)(sourcePath);
89
+ return {
90
+ kind: "bundle",
91
+ sourcePath,
92
+ entries: parsed.entries,
93
+ manifest: parsed.manifest,
94
+ zipSha256: (0, bundle_1.sha256Buffer)(fs_1.default.readFileSync(sourcePath)),
95
+ };
96
+ }
97
+ function resolveTargetRoot(root, target) {
98
+ let contained;
99
+ try {
100
+ contained = (0, workspace_path_1.normalizeContainedWorkspacePath)(target, "--target");
101
+ }
102
+ catch (err) {
103
+ throw new errors_1.UsageError(err instanceof Error ? err.message : String(err));
104
+ }
105
+ const targetRoot = path_1.default.resolve(root, contained);
106
+ if (!isInside(root, targetRoot)) {
107
+ throw new errors_1.UsageError("--target must stay inside the current mdkg root");
108
+ }
109
+ if (fs_1.default.existsSync(targetRoot)) {
110
+ const entries = fs_1.default.readdirSync(targetRoot);
111
+ if (entries.length > 0) {
112
+ throw new errors_1.UsageError(`target must be empty or absent: ${rel(root, targetRoot)}`);
113
+ }
114
+ }
115
+ return targetRoot;
116
+ }
117
+ function assertSourceNotMutatedByTarget(source, targetRoot) {
118
+ if (source.kind !== "directory" || !source.sourceRoot) {
119
+ return;
120
+ }
121
+ const sourceRoot = path_1.default.resolve(source.sourceRoot);
122
+ if (isInside(sourceRoot, targetRoot)) {
123
+ throw new errors_1.UsageError("target must not be inside the live directory source");
124
+ }
125
+ }
126
+ function writeGraphFiles(targetRoot, source) {
127
+ const filesWritten = [];
128
+ const skippedPaths = [];
129
+ for (const file of source.manifest.files) {
130
+ const safeName = safeZipEntryPath(file.path);
131
+ if (file.kind === "generated_index") {
132
+ skippedPaths.push(safeName);
133
+ continue;
134
+ }
135
+ const data = source.entries.get(file.path);
136
+ if (!data) {
137
+ throw new errors_1.ValidationError(`graph source missing bundled file: ${file.path}`);
138
+ }
139
+ const output = path_1.default.join(targetRoot, safeName);
140
+ fs_1.default.mkdirSync(path_1.default.dirname(output), { recursive: true });
141
+ fs_1.default.writeFileSync(output, data);
142
+ filesWritten.push(safeName);
143
+ }
144
+ return { filesWritten: filesWritten.sort(), skippedPaths: skippedPaths.sort() };
145
+ }
146
+ function indexPathsReceipt(root, result) {
147
+ return {
148
+ nodes: rel(root, result.paths.nodes),
149
+ skills: rel(root, result.paths.skills),
150
+ capabilities: rel(root, result.paths.capabilities),
151
+ subgraphs: rel(root, result.paths.subgraphs),
152
+ sqlite: result.paths.sqlite ? rel(root, result.paths.sqlite) : null,
153
+ };
154
+ }
155
+ function resolveStartGoal(targetRoot, requested) {
156
+ const config = (0, config_1.loadConfig)(targetRoot);
157
+ const index = (0, indexer_1.buildIndex)(targetRoot, config);
158
+ const resolved = (0, qid_1.resolveQid)(index, requested, undefined);
159
+ if (resolved.status !== "ok") {
160
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("goal", requested, resolved, undefined));
161
+ }
162
+ const node = index.nodes[resolved.qid];
163
+ if (!node || node.type !== "goal") {
164
+ throw new errors_1.UsageError(`start goal must resolve to a goal: ${requested}`);
165
+ }
166
+ return node;
167
+ }
168
+ function writeSelectedGoal(targetRoot, qid, id, ws) {
169
+ const statePath = path_1.default.join(targetRoot, ".mdkg", "state", "selected-goal.json");
170
+ const state = {
171
+ qid,
172
+ id,
173
+ ws,
174
+ selected_at: new Date().toISOString(),
175
+ };
176
+ (0, atomic_1.atomicWriteFile)(statePath, `${JSON.stringify(state, null, 2)}\n`);
177
+ return rel(targetRoot, statePath);
178
+ }
179
+ function isWorkMarkdownPath(value) {
180
+ const normalized = value.replace(/\\/g, "/");
181
+ return normalized.startsWith(".mdkg/work/") && normalized.endsWith(".md");
182
+ }
183
+ function numericIdPrefix(value) {
184
+ const match = /^([a-z]+)-([0-9]+)$/.exec(value);
185
+ return match?.[1];
186
+ }
187
+ function normalizeIdPrefix(value) {
188
+ const normalized = value.trim().toLowerCase();
189
+ if (!/^[a-z][a-z0-9_-]*$/.test(normalized)) {
190
+ throw new errors_1.UsageError("--id-prefix must start with a letter and use lowercase letters, numbers, underscore, or dash");
191
+ }
192
+ return normalized;
193
+ }
194
+ function nextNumericId(fromId, usedIds) {
195
+ const prefix = numericIdPrefix(fromId);
196
+ if (!prefix) {
197
+ throw new errors_1.UsageError(`cannot rewrite non-numeric template id without --id-prefix: ${fromId}`);
198
+ }
199
+ let next = 1;
200
+ while (usedIds.has(`${prefix}-${next}`)) {
201
+ next += 1;
202
+ }
203
+ return `${prefix}-${next}`;
204
+ }
205
+ function prefixedId(prefix, fromId, usedIds) {
206
+ let candidate = `${prefix}-${fromId}`.replace(/[^a-z0-9._-]+/g, "-");
207
+ let suffix = 2;
208
+ while (usedIds.has(candidate)) {
209
+ candidate = `${prefix}-${fromId}-${suffix}`.replace(/[^a-z0-9._-]+/g, "-");
210
+ suffix += 1;
211
+ }
212
+ return candidate;
213
+ }
214
+ function rewriteStringValue(value, idMap, pathLabel, field, rewrites) {
215
+ let output = value;
216
+ for (const [fromId, toId] of Array.from(idMap.entries()).sort((a, b) => b[0].length - a[0].length)) {
217
+ const replacements = [
218
+ [new RegExp(`\\broot:${escapeRegExp(fromId)}\\b`, "g"), `root:${toId}`, `root:${fromId}`],
219
+ [
220
+ new RegExp(`(^|[^a-zA-Z0-9._:-])${escapeRegExp(fromId)}(?=$|[^a-zA-Z0-9._:-])`, "g"),
221
+ `$1${toId}`,
222
+ fromId,
223
+ ],
224
+ ];
225
+ for (const [pattern, replacement, loggedFrom] of replacements) {
226
+ let count = 0;
227
+ output = output.replace(pattern, (...args) => {
228
+ count += 1;
229
+ return typeof replacement === "string" ? replacement.replace("$1", String(args[1] ?? "")) : replacement;
230
+ });
231
+ if (count > 0) {
232
+ rewrites.push({
233
+ path: pathLabel,
234
+ field,
235
+ from: loggedFrom,
236
+ to: loggedFrom.startsWith("root:") ? `root:${toId}` : toId,
237
+ count,
238
+ });
239
+ }
240
+ }
241
+ }
242
+ return output;
243
+ }
244
+ function rewriteFrontmatterValue(value, idMap, pathLabel, field, rewrites) {
245
+ if (Array.isArray(value)) {
246
+ return value.map((item) => rewriteStringValue(item, idMap, pathLabel, field, rewrites));
247
+ }
248
+ if (typeof value === "string") {
249
+ return rewriteStringValue(value, idMap, pathLabel, field, rewrites);
250
+ }
251
+ return value;
252
+ }
253
+ function targetPathForImport(sourcePath, fromId, toId, usedPaths) {
254
+ const basename = path_1.default.posix.basename(sourcePath);
255
+ const suffix = basename.startsWith(fromId) ? basename.slice(fromId.length) : ".md";
256
+ let candidate = `.mdkg/work/${toId}${suffix}`;
257
+ let count = 2;
258
+ while (usedPaths.has(candidate)) {
259
+ candidate = `.mdkg/work/${toId}-${count}.md`;
260
+ count += 1;
261
+ }
262
+ usedPaths.add(candidate);
263
+ return candidate;
264
+ }
265
+ function renderNode(frontmatter, body) {
266
+ return ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---", body].join("\n");
267
+ }
268
+ function localIdsAndPaths(root) {
269
+ const config = (0, config_1.loadConfig)(root);
270
+ const index = (0, indexer_1.buildIndex)(root, config);
271
+ return {
272
+ ids: new Set(Object.values(index.nodes).filter((node) => !node.source?.imported).map((node) => node.id)),
273
+ paths: new Set(Object.values(index.nodes).filter((node) => !node.source?.imported).map((node) => node.path)),
274
+ };
275
+ }
276
+ function planImportTemplate(options) {
277
+ if (options.dryRun && options.apply) {
278
+ throw new errors_1.UsageError("choose either --dry-run or --apply, not both");
279
+ }
280
+ if (options.selectGoal && !options.startGoal) {
281
+ throw new errors_1.UsageError("--select-goal requires --start-goal <goal-id>");
282
+ }
283
+ const source = loadGraphSource(options.root, options.source);
284
+ const idPrefix = options.idPrefix ? normalizeIdPrefix(options.idPrefix) : undefined;
285
+ const { ids: usedIds, paths: usedPaths } = localIdsAndPaths(options.root);
286
+ const workFiles = source.manifest.files
287
+ .map((file) => safeZipEntryPath(file.path))
288
+ .filter(isWorkMarkdownPath)
289
+ .sort();
290
+ const skippedPaths = source.manifest.files
291
+ .map((file) => safeZipEntryPath(file.path))
292
+ .filter((filePath) => !isWorkMarkdownPath(filePath))
293
+ .sort();
294
+ const imported = workFiles.map((sourcePath) => {
295
+ const data = source.entries.get(sourcePath);
296
+ if (!data) {
297
+ throw new errors_1.ValidationError(`graph source missing bundled file: ${sourcePath}`);
298
+ }
299
+ const parsed = (0, frontmatter_1.parseFrontmatter)(data.toString("utf8"), sourcePath);
300
+ const id = typeof parsed.frontmatter.id === "string" ? parsed.frontmatter.id : undefined;
301
+ const type = typeof parsed.frontmatter.type === "string" ? parsed.frontmatter.type : undefined;
302
+ if (!id || !type) {
303
+ throw new errors_1.ValidationError(`${sourcePath}: imported node requires string id and type`);
304
+ }
305
+ return { sourcePath, parsed, id, type };
306
+ });
307
+ const idMap = new Map();
308
+ for (const node of imported) {
309
+ const mustRewrite = usedIds.has(node.id) || Boolean(numericIdPrefix(node.id));
310
+ if (mustRewrite && !numericIdPrefix(node.id) && !idPrefix) {
311
+ throw new errors_1.UsageError(`cannot rewrite non-numeric template id without --id-prefix: ${node.id}`);
312
+ }
313
+ const toId = mustRewrite
314
+ ? numericIdPrefix(node.id)
315
+ ? nextNumericId(node.id, usedIds)
316
+ : prefixedId(idPrefix, node.id, usedIds)
317
+ : node.id;
318
+ usedIds.add(toId);
319
+ idMap.set(node.id, toId);
320
+ }
321
+ const rewrittenRefs = [];
322
+ const plans = imported.map((node) => {
323
+ const toId = idMap.get(node.id) ?? node.id;
324
+ const frontmatter = { ...node.parsed.frontmatter, id: toId };
325
+ for (const [field, value] of Object.entries(frontmatter)) {
326
+ if (field === "id") {
327
+ continue;
328
+ }
329
+ frontmatter[field] = rewriteFrontmatterValue(value, idMap, node.sourcePath, field, rewrittenRefs);
330
+ }
331
+ const body = rewriteStringValue(node.parsed.body, idMap, node.sourcePath, "body", rewrittenRefs);
332
+ const targetPath = targetPathForImport(node.sourcePath, node.id, toId, usedPaths);
333
+ const targetAbs = path_1.default.resolve(options.root, targetPath);
334
+ if (fs_1.default.existsSync(targetAbs)) {
335
+ throw new errors_1.UsageError(`import target already exists after rewrite: ${targetPath}`);
336
+ }
337
+ return {
338
+ source_path: node.sourcePath,
339
+ target_path: targetPath,
340
+ from_id: node.id,
341
+ to_id: toId,
342
+ type: node.type,
343
+ title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
344
+ content: renderNode(frontmatter, body),
345
+ };
346
+ });
347
+ const startGoalToId = options.startGoal ? (idMap.get(options.startGoal) ?? options.startGoal) : undefined;
348
+ if (options.startGoal && !plans.some((plan) => plan.to_id === startGoalToId && plan.type === "goal")) {
349
+ throw new errors_1.NotFoundError(`start goal not found in imported template graph: ${options.startGoal}`);
350
+ }
351
+ const mode = options.apply ? "import_template_applied" : "import_template_dry_run";
352
+ return {
353
+ action: "graph.import_template",
354
+ ok: true,
355
+ mode,
356
+ source: {
357
+ kind: source.kind,
358
+ path: rel(options.root, source.sourcePath),
359
+ ...(source.sourceRoot ? { source_root: rel(options.root, source.sourceRoot) } : {}),
360
+ profile: source.manifest.profile,
361
+ selected_workspaces: source.manifest.selected_workspaces,
362
+ },
363
+ source_hash: {
364
+ source_tree_hash: source.manifest.source_tree_hash,
365
+ bundle_hash: source.manifest.bundle_hash,
366
+ zip_sha256: source.zipSha256,
367
+ },
368
+ preserved_ids: false,
369
+ rewritten_ids: plans.map((plan) => ({
370
+ from_id: plan.from_id,
371
+ to_id: plan.to_id,
372
+ from_path: plan.source_path,
373
+ to_path: plan.target_path,
374
+ reason: plan.from_id === plan.to_id ? "preserved_non_colliding_id" : "same_repo_import_rewrite",
375
+ })),
376
+ rewritten_refs: rewrittenRefs.sort((a, b) => `${a.path}:${a.field}:${a.from}`.localeCompare(`${b.path}:${b.field}:${b.from}`)),
377
+ planned_paths: plans.map((plan) => plan.target_path).sort(),
378
+ files_written: [],
379
+ skipped_paths: skippedPaths,
380
+ ...(options.startGoal && startGoalToId
381
+ ? { start_goal: { requested: options.startGoal, from_qid: `root:${options.startGoal}`, to_qid: `root:${startGoalToId}` } }
382
+ : {}),
383
+ ...(options.selectGoal && startGoalToId
384
+ ? { selected_goal: { qid: `root:${startGoalToId}`, path: ".mdkg/state/selected-goal.json", planned: !options.apply } }
385
+ : {}),
386
+ warnings: [],
387
+ };
388
+ }
389
+ function applyImportTemplate(options, receipt) {
390
+ const config = (0, config_1.loadConfig)(options.root);
391
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => {
392
+ for (const plan of receipt.rewritten_ids) {
393
+ const source = receipt.planned_paths.find((targetPath) => targetPath === plan.to_path);
394
+ if (!source) {
395
+ throw new errors_1.UsageError(`import plan missing target path for ${plan.from_id}`);
396
+ }
397
+ }
398
+ const source = loadGraphSource(options.root, options.source);
399
+ const idPrefix = options.idPrefix ? normalizeIdPrefix(options.idPrefix) : undefined;
400
+ const applyPlan = planImportTemplate({ ...options, apply: true, dryRun: false, idPrefix });
401
+ const files = applyPlan.planned_paths;
402
+ const workFiles = source.manifest.files
403
+ .map((file) => safeZipEntryPath(file.path))
404
+ .filter(isWorkMarkdownPath)
405
+ .sort();
406
+ const contentByTarget = new Map();
407
+ const { ids: usedIds, paths: usedPaths } = localIdsAndPaths(options.root);
408
+ const imported = workFiles.map((sourcePath) => {
409
+ const data = source.entries.get(sourcePath);
410
+ if (!data) {
411
+ throw new errors_1.ValidationError(`graph source missing bundled file: ${sourcePath}`);
412
+ }
413
+ const parsed = (0, frontmatter_1.parseFrontmatter)(data.toString("utf8"), sourcePath);
414
+ const id = String(parsed.frontmatter.id);
415
+ return { sourcePath, parsed, id };
416
+ });
417
+ const idMap = new Map();
418
+ for (const node of imported) {
419
+ const mustRewrite = usedIds.has(node.id) || Boolean(numericIdPrefix(node.id));
420
+ if (mustRewrite && !numericIdPrefix(node.id) && !idPrefix) {
421
+ throw new errors_1.UsageError(`cannot rewrite non-numeric template id without --id-prefix: ${node.id}`);
422
+ }
423
+ const toId = mustRewrite
424
+ ? numericIdPrefix(node.id)
425
+ ? nextNumericId(node.id, usedIds)
426
+ : prefixedId(idPrefix, node.id, usedIds)
427
+ : node.id;
428
+ usedIds.add(toId);
429
+ idMap.set(node.id, toId);
430
+ }
431
+ const ignoredRewrites = [];
432
+ for (const node of imported) {
433
+ const toId = idMap.get(node.id) ?? node.id;
434
+ const frontmatter = { ...node.parsed.frontmatter, id: toId };
435
+ for (const [field, value] of Object.entries(frontmatter)) {
436
+ if (field === "id") {
437
+ continue;
438
+ }
439
+ frontmatter[field] = rewriteFrontmatterValue(value, idMap, node.sourcePath, field, ignoredRewrites);
440
+ }
441
+ const body = rewriteStringValue(node.parsed.body, idMap, node.sourcePath, "body", ignoredRewrites);
442
+ const targetPath = targetPathForImport(node.sourcePath, node.id, toId, usedPaths);
443
+ contentByTarget.set(targetPath, renderNode(frontmatter, body));
444
+ }
445
+ for (const targetPath of files) {
446
+ const content = contentByTarget.get(targetPath);
447
+ if (!content) {
448
+ throw new errors_1.UsageError(`import plan content missing for ${targetPath}`);
449
+ }
450
+ const targetAbs = path_1.default.resolve(options.root, targetPath);
451
+ fs_1.default.mkdirSync(path_1.default.dirname(targetAbs), { recursive: true });
452
+ (0, atomic_1.atomicWriteFile)(targetAbs, content);
453
+ }
454
+ const indexReceipt = (0, index_1.rebuildDerivedIndexCaches)({ root: options.root });
455
+ if (options.selectGoal && options.startGoal) {
456
+ const selected = applyPlan.selected_goal?.qid;
457
+ if (!selected) {
458
+ throw new errors_1.UsageError("--select-goal could not resolve imported start goal");
459
+ }
460
+ const [, id] = selected.split(":");
461
+ if (!id) {
462
+ throw new errors_1.UsageError(`invalid selected goal qid: ${selected}`);
463
+ }
464
+ writeSelectedGoal(options.root, selected, id, "root");
465
+ applyPlan.selected_goal = { qid: selected, path: ".mdkg/state/selected-goal.json", planned: false };
466
+ }
467
+ const validation = (0, validate_1.collectValidateReceipt)({ root: options.root, quiet: true });
468
+ if (validation.error_count > 0) {
469
+ throw new errors_1.ValidationError(`imported graph validation failed with ${validation.error_count} error(s)`);
470
+ }
471
+ return {
472
+ ...applyPlan,
473
+ files_written: files,
474
+ index: {
475
+ rebuilt: true,
476
+ paths: indexPathsReceipt(options.root, indexReceipt),
477
+ },
478
+ validation,
479
+ };
480
+ });
481
+ }
482
+ function runGraphTransport(options, mode) {
483
+ const source = loadGraphSource(options.root, options.source);
484
+ const targetRoot = resolveTargetRoot(options.root, options.target);
485
+ assertSourceNotMutatedByTarget(source, targetRoot);
486
+ const warnings = [];
487
+ fs_1.default.mkdirSync(targetRoot, { recursive: true });
488
+ const { filesWritten, skippedPaths } = writeGraphFiles(targetRoot, source);
489
+ const indexReceipt = (0, index_1.rebuildDerivedIndexCaches)({ root: targetRoot });
490
+ let validation = (0, validate_1.collectValidateReceipt)({ root: targetRoot, quiet: true });
491
+ if (validation.error_count > 0) {
492
+ throw new errors_1.ValidationError(`cloned graph validation failed with ${validation.error_count} error(s)`);
493
+ }
494
+ let startGoal;
495
+ let selectedGoal;
496
+ if (mode === "fork" && options.startGoal) {
497
+ const node = resolveStartGoal(targetRoot, options.startGoal);
498
+ const statePath = writeSelectedGoal(targetRoot, node.qid, node.id, node.ws);
499
+ startGoal = {
500
+ requested: options.startGoal,
501
+ qid: node.qid,
502
+ path: node.path,
503
+ };
504
+ selectedGoal = {
505
+ qid: node.qid,
506
+ path: statePath,
507
+ };
508
+ validation = (0, validate_1.collectValidateReceipt)({ root: targetRoot, quiet: true });
509
+ if (validation.error_count > 0) {
510
+ throw new errors_1.ValidationError(`forked graph validation failed with ${validation.error_count} error(s)`);
511
+ }
512
+ }
513
+ else if (mode === "clone" && options.startGoal) {
514
+ warnings.push("--start-goal is ignored by graph clone; use graph fork for start-goal selection");
515
+ }
516
+ return {
517
+ action: mode === "clone" ? "graph.clone" : "graph.fork",
518
+ ok: true,
519
+ mode,
520
+ source: {
521
+ kind: source.kind,
522
+ path: rel(options.root, source.sourcePath),
523
+ ...(source.sourceRoot ? { source_root: rel(options.root, source.sourceRoot) } : {}),
524
+ profile: source.manifest.profile,
525
+ selected_workspaces: source.manifest.selected_workspaces,
526
+ },
527
+ target: rel(options.root, targetRoot),
528
+ source_hash: {
529
+ source_tree_hash: source.manifest.source_tree_hash,
530
+ bundle_hash: source.manifest.bundle_hash,
531
+ zip_sha256: source.zipSha256,
532
+ },
533
+ preserved_ids: true,
534
+ files_written: filesWritten,
535
+ skipped_paths: skippedPaths,
536
+ ...(startGoal ? { start_goal: startGoal } : {}),
537
+ ...(selectedGoal ? { selected_goal: selectedGoal } : {}),
538
+ index: {
539
+ rebuilt: true,
540
+ paths: indexPathsReceipt(targetRoot, indexReceipt),
541
+ },
542
+ validation,
543
+ warnings,
544
+ };
545
+ }
546
+ function printReceipt(receipt, json) {
547
+ if (json) {
548
+ writeJson(receipt);
549
+ return;
550
+ }
551
+ console.log(`${receipt.action}: ${receipt.target}`);
552
+ console.log(`source: ${receipt.source.path}`);
553
+ console.log(`files: ${receipt.files_written.length}`);
554
+ console.log(`preserved_ids: ${receipt.preserved_ids}`);
555
+ if (receipt.start_goal) {
556
+ console.log(`start_goal: ${receipt.start_goal.qid}`);
557
+ }
558
+ if (receipt.warnings.length > 0) {
559
+ console.log(`warnings: ${receipt.warnings.length}`);
560
+ }
561
+ }
562
+ function runGraphCloneCommand(options) {
563
+ printReceipt(runGraphTransport(options, "clone"), options.json);
564
+ }
565
+ function runGraphForkCommand(options) {
566
+ printReceipt(runGraphTransport(options, "fork"), options.json);
567
+ }
568
+ function runGraphImportTemplateCommand(options) {
569
+ const plan = planImportTemplate(options);
570
+ const receipt = options.apply ? applyImportTemplate(options, plan) : plan;
571
+ if (options.json) {
572
+ writeJson(receipt);
573
+ return;
574
+ }
575
+ console.log(`${receipt.action}: ${receipt.mode}`);
576
+ console.log(`source: ${receipt.source.path}`);
577
+ console.log(`planned_paths: ${receipt.planned_paths.length}`);
578
+ console.log(`rewritten_ids: ${receipt.rewritten_ids.filter((item) => item.from_id !== item.to_id).length}`);
579
+ if (receipt.selected_goal) {
580
+ console.log(`selected_goal: ${receipt.selected_goal.qid}${receipt.selected_goal.planned ? " (planned)" : ""}`);
581
+ }
582
+ }
@@ -3,6 +3,7 @@ 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.collectValidateReceipt = collectValidateReceipt;
6
7
  exports.runValidateCommand = runValidateCommand;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -244,7 +245,7 @@ function validateEventsJsonl(root, config, errors) {
244
245
  }
245
246
  }
246
247
  }
247
- function runValidateCommand(options) {
248
+ function collectValidateReceipt(options) {
248
249
  const config = (0, config_1.loadConfig)(options.root);
249
250
  const templateSchemaInfo = (0, template_schema_1.loadTemplateSchemasWithInfo)(options.root, config, node_1.ALLOWED_TYPES);
250
251
  const templateSchemas = templateSchemaInfo.schemas;
@@ -392,31 +393,35 @@ function runValidateCommand(options) {
392
393
  errors: uniqueErrors,
393
394
  ...(outPath ? { report_path: outPath } : {}),
394
395
  };
396
+ return receipt;
397
+ }
398
+ function runValidateCommand(options) {
399
+ const receipt = collectValidateReceipt(options);
395
400
  if (options.json) {
396
401
  console.log(JSON.stringify(receipt, null, 2));
397
- if (uniqueErrors.length > 0) {
398
- throw new errors_1.ValidationError(`validation failed with ${uniqueErrors.length} error(s)`);
402
+ if (receipt.error_count > 0) {
403
+ throw new errors_1.ValidationError(`validation failed with ${receipt.error_count} error(s)`);
399
404
  }
400
405
  return;
401
406
  }
402
407
  if (!options.quiet) {
403
- for (const warning of uniqueWarnings) {
408
+ for (const warning of receipt.warnings) {
404
409
  console.error(`warning: ${warning}`);
405
410
  }
406
411
  }
407
- if (uniqueErrors.length > 0) {
408
- if (outPath) {
409
- console.error(`validation failed: ${uniqueErrors.length} error(s). details written to ${outPath}`);
412
+ if (receipt.error_count > 0) {
413
+ if (receipt.report_path) {
414
+ console.error(`validation failed: ${receipt.error_count} error(s). details written to ${receipt.report_path}`);
410
415
  }
411
416
  else {
412
- for (const error of uniqueErrors) {
417
+ for (const error of receipt.errors) {
413
418
  console.error(error);
414
419
  }
415
420
  }
416
- throw new errors_1.ValidationError(`validation failed with ${uniqueErrors.length} error(s)`);
421
+ throw new errors_1.ValidationError(`validation failed with ${receipt.error_count} error(s)`);
417
422
  }
418
- if (outPath) {
419
- console.log(`validation report written: ${outPath}`);
423
+ if (receipt.report_path) {
424
+ console.log(`validation report written: ${receipt.report_path}`);
420
425
  }
421
426
  console.log("validation ok");
422
427
  }
@@ -22,6 +22,8 @@ Primary commands:
22
22
  - `mdkg spec`
23
23
  - `mdkg archive`
24
24
  - `mdkg bundle`
25
+ - `mdkg graph`
26
+ - `mdkg subgraph`
25
27
  - `mdkg work`
26
28
  - `mdkg goal`
27
29
  - `mdkg task`
@@ -204,6 +206,21 @@ Graph snapshot bundles:
204
206
  - public bundles include only public workspace content and public archive sidecars
205
207
  - public bundle creation fails when public records reference private graph, archive, or subgraph records
206
208
 
209
+ Graph clone, fork, and template import:
210
+ - `mdkg graph clone <source-bundle-or-mdkg-dir> --target <path> [--json]`
211
+ - `mdkg graph fork <source-bundle-or-mdkg-dir> --target <path> [--start-goal <goal-id>] [--json]`
212
+ - `mdkg graph import-template <source-bundle-or-mdkg-dir> [--start-goal <goal-id>] [--select-goal] [--id-prefix <prefix>] [--dry-run] [--apply] [--json]`
213
+ - `graph clone` and `graph fork` preserve IDs because the target is a separate graph namespace
214
+ - clone/fork targets must be empty or absent and stay under the current mdkg root
215
+ - live directory sources are never mutated; clone/fork refuses targets nested inside a live source directory
216
+ - `graph fork --start-goal <goal-id>` writes selected-goal state in the target graph after validation
217
+ - `graph import-template` imports authored `.mdkg/work/*.md` template nodes into the current repo and skips config, generated indexes, archive payloads, bundles, and materialized subgraph views
218
+ - `graph import-template` defaults to dry-run unless `--apply` is supplied
219
+ - same-repo template import rewrites canonical numeric IDs to the next unused ID by type prefix and rewrites structured refs plus safe body-local id/qid mentions
220
+ - colliding semantic template IDs require `--id-prefix`
221
+ - `--select-goal` requires `--start-goal` and writes selected-goal state only after apply validation
222
+ - subgraphs remain read-only bundle projections for orchestration context; use `graph clone|fork|import-template` when authored graph state should be created
223
+
207
224
  Subgraph orchestration:
208
225
  - `mdkg subgraph add <alias> <bundle-path> [--visibility private|internal|public] [--profile private|public] [--source-path <path>] [--json]`
209
226
  - `mdkg subgraph list [--json]`