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.
@@ -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
- if (options.startGoal && !plans.some((plan) => plan.to_id === startGoalToId && plan.type === "goal")) {
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
- warnings: [],
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 [, id] = selected.split(":");
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
- 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)`);
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,