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.
- package/CHANGELOG.md +41 -0
- package/CLI_COMMAND_MATRIX.md +42 -2
- package/README.md +22 -1
- package/dist/cli.js +89 -0
- package/dist/command-contract.json +353 -2
- package/dist/commands/graph.js +582 -0
- package/dist/commands/validate.js +16 -11
- package/dist/init/CLI_COMMAND_MATRIX.md +17 -0
- package/dist/init/README.md +18 -1
- package/dist/init/init-manifest.json +3 -3
- package/dist/util/argparse.js +3 -0
- package/package.json +3 -2
|
@@ -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
|
|
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 (
|
|
398
|
-
throw new errors_1.ValidationError(`validation failed with ${
|
|
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
|
|
408
|
+
for (const warning of receipt.warnings) {
|
|
404
409
|
console.error(`warning: ${warning}`);
|
|
405
410
|
}
|
|
406
411
|
}
|
|
407
|
-
if (
|
|
408
|
-
if (
|
|
409
|
-
console.error(`validation failed: ${
|
|
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
|
|
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 ${
|
|
421
|
+
throw new errors_1.ValidationError(`validation failed with ${receipt.error_count} error(s)`);
|
|
417
422
|
}
|
|
418
|
-
if (
|
|
419
|
-
console.log(`validation report written: ${
|
|
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]`
|