mdkg 0.3.5 → 0.3.6
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 +28 -2
- package/CLI_COMMAND_MATRIX.md +34 -3
- package/README.md +21 -4
- package/dist/cli.js +90 -2
- package/dist/command-contract.json +158 -5
- package/dist/commands/graph.js +132 -10
- package/dist/commands/mcp.js +647 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +12 -1
- package/dist/init/README.md +9 -1
- package/dist/init/init-manifest.json +3 -3
- package/dist/util/argparse.js +1 -0
- package/package.json +3 -2
package/dist/commands/graph.js
CHANGED
|
@@ -20,6 +20,7 @@ const qid_1 = require("../util/qid");
|
|
|
20
20
|
const atomic_1 = require("../util/atomic");
|
|
21
21
|
const zip_1 = require("../util/zip");
|
|
22
22
|
const lock_1 = require("../util/lock");
|
|
23
|
+
const date_1 = require("../util/date");
|
|
23
24
|
function writeJson(value) {
|
|
24
25
|
console.log(JSON.stringify(value, null, 2));
|
|
25
26
|
}
|
|
@@ -176,6 +177,85 @@ function writeSelectedGoal(targetRoot, qid, id, ws) {
|
|
|
176
177
|
(0, atomic_1.atomicWriteFile)(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
177
178
|
return rel(targetRoot, statePath);
|
|
178
179
|
}
|
|
180
|
+
function qidForRoot(id) {
|
|
181
|
+
return `root:${id}`;
|
|
182
|
+
}
|
|
183
|
+
function idFromRootQid(qid) {
|
|
184
|
+
const [workspace, id] = qid.split(":");
|
|
185
|
+
if (workspace !== "root" || !id) {
|
|
186
|
+
throw new errors_1.UsageError(`invalid root qid: ${qid}`);
|
|
187
|
+
}
|
|
188
|
+
return id;
|
|
189
|
+
}
|
|
190
|
+
function ensureStatusAllowed(config, status) {
|
|
191
|
+
const normalized = status.toLowerCase();
|
|
192
|
+
const allowed = new Set(config.work.status_enum.map((value) => value.toLowerCase()));
|
|
193
|
+
if (!allowed.has(normalized)) {
|
|
194
|
+
throw new errors_1.UsageError(`goal status ${normalized} is not allowed by work.status_enum`);
|
|
195
|
+
}
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
function isActiveGoalStatus(status, goalState) {
|
|
199
|
+
return status === "progress" && goalState === "active";
|
|
200
|
+
}
|
|
201
|
+
function isClosedGoalStatus(status, goalState) {
|
|
202
|
+
return status === "done" || status === "archived" || goalState === "achieved" || goalState === "archived";
|
|
203
|
+
}
|
|
204
|
+
function activeLocalRootGoals(root) {
|
|
205
|
+
const config = (0, config_1.loadConfig)(root);
|
|
206
|
+
const index = (0, indexer_1.buildIndex)(root, config);
|
|
207
|
+
return Object.values(index.nodes)
|
|
208
|
+
.filter((node) => !node.source?.imported)
|
|
209
|
+
.filter((node) => node.ws === "root" && node.type === "goal")
|
|
210
|
+
.filter((node) => isActiveGoalStatus(node.status, String(node.attributes.goal_state ?? "")))
|
|
211
|
+
.sort((a, b) => a.qid.localeCompare(b.qid));
|
|
212
|
+
}
|
|
213
|
+
function localGoalLifecycleReceipt(node, status, goalState, planned) {
|
|
214
|
+
return {
|
|
215
|
+
workspace: node.ws,
|
|
216
|
+
id: node.id,
|
|
217
|
+
qid: node.qid,
|
|
218
|
+
path: node.path,
|
|
219
|
+
previous_status: node.status ?? "",
|
|
220
|
+
previous_goal_state: String(node.attributes.goal_state ?? ""),
|
|
221
|
+
status,
|
|
222
|
+
goal_state: goalState,
|
|
223
|
+
source: "local",
|
|
224
|
+
planned,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function importedGoalLifecycleReceipt(plan, status, goalState, planned) {
|
|
228
|
+
return {
|
|
229
|
+
workspace: "root",
|
|
230
|
+
id: plan.to_id,
|
|
231
|
+
qid: qidForRoot(plan.to_id),
|
|
232
|
+
path: plan.target_path,
|
|
233
|
+
previous_status: plan.status ?? "",
|
|
234
|
+
previous_goal_state: plan.goal_state ?? "",
|
|
235
|
+
status,
|
|
236
|
+
goal_state: goalState,
|
|
237
|
+
source: "imported",
|
|
238
|
+
planned,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function readNodeFile(root, nodePath) {
|
|
242
|
+
const filePath = path_1.default.join(root, nodePath);
|
|
243
|
+
const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(filePath, "utf8"), nodePath);
|
|
244
|
+
return { filePath, frontmatter: { ...parsed.frontmatter }, body: parsed.body };
|
|
245
|
+
}
|
|
246
|
+
function writeRenderedNodeFile(filePath, frontmatter, body) {
|
|
247
|
+
(0, atomic_1.atomicWriteFile)(filePath, renderNode(frontmatter, body));
|
|
248
|
+
}
|
|
249
|
+
function pauseLocalGoals(root, goals, config) {
|
|
250
|
+
const today = (0, date_1.formatDate)(new Date());
|
|
251
|
+
for (const goal of goals.filter((item) => item.source === "local")) {
|
|
252
|
+
const loaded = readNodeFile(root, goal.path);
|
|
253
|
+
loaded.frontmatter.status = ensureStatusAllowed(config, "blocked");
|
|
254
|
+
loaded.frontmatter.goal_state = "paused";
|
|
255
|
+
loaded.frontmatter.updated = today;
|
|
256
|
+
writeRenderedNodeFile(loaded.filePath, loaded.frontmatter, loaded.body);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
179
259
|
function isWorkMarkdownPath(value) {
|
|
180
260
|
const normalized = value.replace(/\\/g, "/");
|
|
181
261
|
return normalized.startsWith(".mdkg/work/") && normalized.endsWith(".md");
|
|
@@ -340,14 +420,41 @@ function planImportTemplate(options) {
|
|
|
340
420
|
from_id: node.id,
|
|
341
421
|
to_id: toId,
|
|
342
422
|
type: node.type,
|
|
423
|
+
status: typeof frontmatter.status === "string" ? frontmatter.status : undefined,
|
|
424
|
+
goal_state: typeof frontmatter.goal_state === "string" ? frontmatter.goal_state : undefined,
|
|
343
425
|
title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
|
|
344
426
|
content: renderNode(frontmatter, body),
|
|
345
427
|
};
|
|
346
428
|
});
|
|
347
429
|
const startGoalToId = options.startGoal ? (idMap.get(options.startGoal) ?? options.startGoal) : undefined;
|
|
348
|
-
|
|
430
|
+
const startGoalPlan = startGoalToId
|
|
431
|
+
? plans.find((plan) => plan.to_id === startGoalToId && plan.type === "goal")
|
|
432
|
+
: undefined;
|
|
433
|
+
if (options.startGoal && !startGoalPlan) {
|
|
349
434
|
throw new errors_1.NotFoundError(`start goal not found in imported template graph: ${options.startGoal}`);
|
|
350
435
|
}
|
|
436
|
+
if (options.selectGoal && startGoalPlan && isClosedGoalStatus(startGoalPlan.status, startGoalPlan.goal_state)) {
|
|
437
|
+
throw new errors_1.UsageError(`cannot select achieved or archived imported start goal: ${options.startGoal}`);
|
|
438
|
+
}
|
|
439
|
+
const localActiveGoals = activeLocalRootGoals(options.root);
|
|
440
|
+
const importedActiveGoals = plans
|
|
441
|
+
.filter((plan) => plan.type === "goal")
|
|
442
|
+
.filter((plan) => isActiveGoalStatus(plan.status, plan.goal_state));
|
|
443
|
+
if (!options.selectGoal && localActiveGoals.length + importedActiveGoals.length > 1) {
|
|
444
|
+
throw new errors_1.UsageError("import-template would create multiple active root goals; use --select-goal --start-goal <goal-id> or pause active goals before importing");
|
|
445
|
+
}
|
|
446
|
+
const activatedGoal = options.selectGoal && startGoalPlan
|
|
447
|
+
? importedGoalLifecycleReceipt(startGoalPlan, "progress", "active", !options.apply)
|
|
448
|
+
: undefined;
|
|
449
|
+
const pausedGoals = options.selectGoal && startGoalPlan
|
|
450
|
+
? [
|
|
451
|
+
...localActiveGoals.map((node) => localGoalLifecycleReceipt(node, "blocked", "paused", !options.apply)),
|
|
452
|
+
...importedActiveGoals
|
|
453
|
+
.filter((plan) => plan.to_id !== startGoalPlan.to_id)
|
|
454
|
+
.map((plan) => importedGoalLifecycleReceipt(plan, "blocked", "paused", !options.apply)),
|
|
455
|
+
]
|
|
456
|
+
: [];
|
|
457
|
+
const warnings = pausedGoals.length > 0 ? [`paused ${pausedGoals.length} competing active goal(s)`] : [];
|
|
351
458
|
const mode = options.apply ? "import_template_applied" : "import_template_dry_run";
|
|
352
459
|
return {
|
|
353
460
|
action: "graph.import_template",
|
|
@@ -383,7 +490,9 @@ function planImportTemplate(options) {
|
|
|
383
490
|
...(options.selectGoal && startGoalToId
|
|
384
491
|
? { selected_goal: { qid: `root:${startGoalToId}`, path: ".mdkg/state/selected-goal.json", planned: !options.apply } }
|
|
385
492
|
: {}),
|
|
386
|
-
|
|
493
|
+
...(activatedGoal ? { activated_goal: activatedGoal } : {}),
|
|
494
|
+
paused_goals: pausedGoals,
|
|
495
|
+
warnings,
|
|
387
496
|
};
|
|
388
497
|
}
|
|
389
498
|
function applyImportTemplate(options, receipt) {
|
|
@@ -439,6 +548,17 @@ function applyImportTemplate(options, receipt) {
|
|
|
439
548
|
frontmatter[field] = rewriteFrontmatterValue(value, idMap, node.sourcePath, field, ignoredRewrites);
|
|
440
549
|
}
|
|
441
550
|
const body = rewriteStringValue(node.parsed.body, idMap, node.sourcePath, "body", ignoredRewrites);
|
|
551
|
+
if (frontmatter.type === "goal" && options.selectGoal) {
|
|
552
|
+
if (qidForRoot(toId) === applyPlan.activated_goal?.qid) {
|
|
553
|
+
frontmatter.status = ensureStatusAllowed(config, "progress");
|
|
554
|
+
frontmatter.goal_state = "active";
|
|
555
|
+
}
|
|
556
|
+
else if (isActiveGoalStatus(String(frontmatter.status ?? ""), String(frontmatter.goal_state ?? ""))) {
|
|
557
|
+
frontmatter.status = ensureStatusAllowed(config, "blocked");
|
|
558
|
+
frontmatter.goal_state = "paused";
|
|
559
|
+
}
|
|
560
|
+
frontmatter.updated = (0, date_1.formatDate)(new Date());
|
|
561
|
+
}
|
|
442
562
|
const targetPath = targetPathForImport(node.sourcePath, node.id, toId, usedPaths);
|
|
443
563
|
contentByTarget.set(targetPath, renderNode(frontmatter, body));
|
|
444
564
|
}
|
|
@@ -451,22 +571,24 @@ function applyImportTemplate(options, receipt) {
|
|
|
451
571
|
fs_1.default.mkdirSync(path_1.default.dirname(targetAbs), { recursive: true });
|
|
452
572
|
(0, atomic_1.atomicWriteFile)(targetAbs, content);
|
|
453
573
|
}
|
|
574
|
+
pauseLocalGoals(options.root, applyPlan.paused_goals, config);
|
|
454
575
|
const indexReceipt = (0, index_1.rebuildDerivedIndexCaches)({ root: options.root });
|
|
576
|
+
const validation = (0, validate_1.collectValidateReceipt)({ root: options.root, quiet: true });
|
|
577
|
+
if (validation.error_count > 0) {
|
|
578
|
+
throw new errors_1.ValidationError(`imported graph validation failed with ${validation.error_count} error(s)`);
|
|
579
|
+
}
|
|
455
580
|
if (options.selectGoal && options.startGoal) {
|
|
456
581
|
const selected = applyPlan.selected_goal?.qid;
|
|
457
582
|
if (!selected) {
|
|
458
583
|
throw new errors_1.UsageError("--select-goal could not resolve imported start goal");
|
|
459
584
|
}
|
|
460
|
-
const
|
|
461
|
-
if (!id) {
|
|
462
|
-
throw new errors_1.UsageError(`invalid selected goal qid: ${selected}`);
|
|
463
|
-
}
|
|
585
|
+
const id = idFromRootQid(selected);
|
|
464
586
|
writeSelectedGoal(options.root, selected, id, "root");
|
|
465
587
|
applyPlan.selected_goal = { qid: selected, path: ".mdkg/state/selected-goal.json", planned: false };
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
588
|
+
if (applyPlan.activated_goal) {
|
|
589
|
+
applyPlan.activated_goal.planned = false;
|
|
590
|
+
}
|
|
591
|
+
applyPlan.paused_goals = applyPlan.paused_goals.map((goal) => ({ ...goal, planned: false }));
|
|
470
592
|
}
|
|
471
593
|
return {
|
|
472
594
|
...applyPlan,
|