mdkg 0.2.0 → 0.3.1

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 (41) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/CLI_COMMAND_MATRIX.md +1176 -0
  3. package/README.md +58 -5
  4. package/dist/cli.js +267 -12
  5. package/dist/command-contract.json +7473 -0
  6. package/dist/commands/capability.js +13 -8
  7. package/dist/commands/doctor.js +370 -86
  8. package/dist/commands/fix.js +924 -0
  9. package/dist/commands/format.js +9 -3
  10. package/dist/commands/skill.js +13 -3
  11. package/dist/commands/skill_support.js +3 -3
  12. package/dist/commands/spec.js +101 -0
  13. package/dist/commands/status.js +270 -0
  14. package/dist/commands/subgraph.js +300 -0
  15. package/dist/commands/validate.js +1 -1
  16. package/dist/commands/work.js +569 -20
  17. package/dist/commands/workspace.js +19 -7
  18. package/dist/graph/agent_file_types.js +95 -7
  19. package/dist/graph/capabilities_indexer.js +89 -2
  20. package/dist/graph/frontmatter.js +6 -0
  21. package/dist/graph/node.js +8 -2
  22. package/dist/init/AGENT_START.md +5 -1
  23. package/dist/init/CLI_COMMAND_MATRIX.md +36 -0
  24. package/dist/init/README.md +41 -2
  25. package/dist/init/init-manifest.json +20 -20
  26. package/dist/init/templates/default/receipt.md +12 -1
  27. package/dist/init/templates/default/spec.md +8 -6
  28. package/dist/init/templates/default/work.md +5 -1
  29. package/dist/init/templates/default/work_order.md +11 -0
  30. package/dist/init/templates/specs/agent.SPEC.md +45 -4
  31. package/dist/init/templates/specs/api.SPEC.md +1 -0
  32. package/dist/init/templates/specs/base.SPEC.md +45 -12
  33. package/dist/init/templates/specs/capability.SPEC.md +16 -3
  34. package/dist/init/templates/specs/integration.SPEC.md +1 -0
  35. package/dist/init/templates/specs/model.SPEC.md +1 -0
  36. package/dist/init/templates/specs/project.SPEC.md +14 -1
  37. package/dist/init/templates/specs/{omniruntime-agent.SPEC.md → runtime-agent.SPEC.md} +13 -3
  38. package/dist/init/templates/specs/runtime-image.SPEC.md +1 -0
  39. package/dist/init/templates/specs/tool.SPEC.md +1 -0
  40. package/dist/util/argparse.js +9 -0
  41. package/package.json +12 -3
@@ -0,0 +1,924 @@
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.collectFixPlan = collectFixPlan;
7
+ exports.runFixPlanCommand = runFixPlanCommand;
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const child_process_1 = require("child_process");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const config_1 = require("../core/config");
13
+ const capabilities_indexer_1 = require("../graph/capabilities_indexer");
14
+ const capabilities_index_cache_1 = require("../graph/capabilities_index_cache");
15
+ const staleness_1 = require("../graph/staleness");
16
+ const skills_index_cache_1 = require("../graph/skills_index_cache");
17
+ const skills_indexer_1 = require("../graph/skills_indexer");
18
+ const sqlite_index_1 = require("../graph/sqlite_index");
19
+ const subgraphs_1 = require("../graph/subgraphs");
20
+ const indexer_1 = require("../graph/indexer");
21
+ const node_1 = require("../graph/node");
22
+ const template_schema_1 = require("../graph/template_schema");
23
+ const workspace_files_1 = require("../graph/workspace_files");
24
+ const errors_1 = require("../util/errors");
25
+ const refs_1 = require("../util/refs");
26
+ const FAMILY_VALUES = new Set(["index", "refs", "ids", "all"]);
27
+ const CONCRETE_FAMILIES = ["index", "refs", "ids"];
28
+ function stableValue(value) {
29
+ if (Array.isArray(value)) {
30
+ return value.map(stableValue);
31
+ }
32
+ if (value && typeof value === "object") {
33
+ const input = value;
34
+ const output = {};
35
+ for (const key of Object.keys(input).sort()) {
36
+ output[key] = stableValue(input[key]);
37
+ }
38
+ return output;
39
+ }
40
+ return value;
41
+ }
42
+ function stableJson(value) {
43
+ return JSON.stringify(stableValue(value));
44
+ }
45
+ function sha256(value) {
46
+ return `sha256:${crypto_1.default.createHash("sha256").update(stableJson(value)).digest("hex")}`;
47
+ }
48
+ function normalizeFamily(value) {
49
+ const normalized = (value ?? "all").toLowerCase();
50
+ if (!FAMILY_VALUES.has(normalized)) {
51
+ throw new errors_1.UsageError("--family must be one of index, refs, ids, all");
52
+ }
53
+ return normalized;
54
+ }
55
+ function selectedFamilies(family) {
56
+ return family === "all" ? [...CONCRETE_FAMILIES] : [family];
57
+ }
58
+ function relativeRoot(root) {
59
+ try {
60
+ return fs_1.default.realpathSync(root);
61
+ }
62
+ catch {
63
+ return path_1.default.resolve(root);
64
+ }
65
+ }
66
+ function rel(root, target) {
67
+ return path_1.default.relative(root, target).split(path_1.default.sep).join("/") || ".";
68
+ }
69
+ function runGit(root, args) {
70
+ const result = (0, child_process_1.spawnSync)("git", args, { cwd: root, encoding: "utf8" });
71
+ if (result.status !== 0) {
72
+ return undefined;
73
+ }
74
+ return result.stdout.trim();
75
+ }
76
+ function collectDirtyState(root) {
77
+ const inside = runGit(root, ["rev-parse", "--is-inside-work-tree"]) === "true";
78
+ if (!inside) {
79
+ return {
80
+ inside: false,
81
+ dirty: false,
82
+ dirty_count: 0,
83
+ untracked_count: 0,
84
+ };
85
+ }
86
+ const porcelain = runGit(root, ["status", "--porcelain"]) ?? "";
87
+ const lines = porcelain.split(/\r?\n/).filter(Boolean);
88
+ return {
89
+ inside: true,
90
+ dirty: lines.length > 0,
91
+ dirty_count: lines.length,
92
+ untracked_count: lines.filter((line) => line.startsWith("??")).length,
93
+ };
94
+ }
95
+ function emptyFamilySummaries(selected) {
96
+ return CONCRETE_FAMILIES.map((family) => ({
97
+ family,
98
+ selected: selected.includes(family),
99
+ proposed_count: 0,
100
+ blocked_count: 0,
101
+ }));
102
+ }
103
+ function readJsonProblem(filePath) {
104
+ if (!fs_1.default.existsSync(filePath)) {
105
+ return undefined;
106
+ }
107
+ try {
108
+ JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
109
+ return undefined;
110
+ }
111
+ catch (err) {
112
+ return err instanceof Error ? err.message : "unreadable json";
113
+ }
114
+ }
115
+ function planIndexRepairs(root) {
116
+ const config = (0, config_1.loadConfig)(root);
117
+ const entries = [
118
+ {
119
+ name: "global-index",
120
+ path: path_1.default.resolve(root, config.index.global_index_path),
121
+ required: true,
122
+ stale: () => (0, staleness_1.isIndexStale)(root, config),
123
+ json: true,
124
+ },
125
+ {
126
+ name: "skills-index",
127
+ path: (0, skills_indexer_1.resolveSkillsIndexPath)(root),
128
+ required: true,
129
+ stale: () => (0, skills_index_cache_1.isSkillsIndexStale)(root, config),
130
+ json: true,
131
+ },
132
+ {
133
+ name: "capabilities-index",
134
+ path: (0, capabilities_indexer_1.resolveCapabilitiesIndexPath)(root, config),
135
+ required: true,
136
+ stale: () => (0, capabilities_index_cache_1.isCapabilitiesIndexStale)(root, config),
137
+ json: true,
138
+ },
139
+ {
140
+ name: "subgraphs-index",
141
+ path: (0, subgraphs_1.resolveSubgraphsIndexPath)(root),
142
+ required: Object.keys(config.subgraphs).length > 0,
143
+ stale: () => (0, subgraphs_1.isSubgraphsIndexStale)(root, config),
144
+ json: true,
145
+ },
146
+ ];
147
+ if ((0, sqlite_index_1.isSqliteBackend)(config)) {
148
+ entries.push({
149
+ name: "sqlite-index",
150
+ path: path_1.default.resolve(root, ".mdkg", "index", "mdkg.sqlite"),
151
+ required: true,
152
+ stale: () => (0, staleness_1.isIndexStale)(root, config),
153
+ json: false,
154
+ });
155
+ }
156
+ const changes = [];
157
+ for (const entry of entries) {
158
+ if (!entry.required) {
159
+ continue;
160
+ }
161
+ const relativePath = rel(root, entry.path);
162
+ let reason;
163
+ let detail;
164
+ if (!fs_1.default.existsSync(entry.path)) {
165
+ reason = "generated_cache_missing";
166
+ detail = { cache: entry.name };
167
+ }
168
+ else if (entry.json) {
169
+ const readProblem = readJsonProblem(entry.path);
170
+ if (readProblem) {
171
+ reason = "generated_cache_unreadable";
172
+ detail = { cache: entry.name, error: readProblem };
173
+ }
174
+ }
175
+ if (!reason) {
176
+ try {
177
+ if (entry.stale()) {
178
+ reason = "generated_cache_stale";
179
+ detail = { cache: entry.name };
180
+ }
181
+ }
182
+ catch (err) {
183
+ reason = "generated_cache_staleness_unknown";
184
+ detail = {
185
+ cache: entry.name,
186
+ error: err instanceof Error ? err.message : String(err),
187
+ };
188
+ }
189
+ }
190
+ if (!reason) {
191
+ continue;
192
+ }
193
+ changes.push({
194
+ id: `index.${String(changes.length + 1).padStart(3, "0")}`,
195
+ family: "index",
196
+ risk: "low",
197
+ status: "planned",
198
+ reason,
199
+ paths: [relativePath],
200
+ refs: [],
201
+ before: detail,
202
+ after: { command: "mdkg index" },
203
+ command_hint: "mdkg index",
204
+ apply_supported: false,
205
+ });
206
+ }
207
+ return { proposed: changes, blocked: [] };
208
+ }
209
+ function edgeEntries(index) {
210
+ const entries = [];
211
+ for (const node of Object.values(index.nodes).sort((a, b) => a.qid.localeCompare(b.qid))) {
212
+ const edgeMap = [
213
+ ["epic", node.edges.epic],
214
+ ["parent", node.edges.parent],
215
+ ["prev", node.edges.prev],
216
+ ["next", node.edges.next],
217
+ ];
218
+ for (const [field, target] of edgeMap) {
219
+ if (target) {
220
+ entries.push({ qid: node.qid, path: node.path, field, target });
221
+ }
222
+ }
223
+ for (const [field, targets] of [
224
+ ["relates", node.edges.relates],
225
+ ["blocked_by", node.edges.blocked_by],
226
+ ["blocks", node.edges.blocks],
227
+ ]) {
228
+ for (const target of targets) {
229
+ entries.push({ qid: node.qid, path: node.path, field, target });
230
+ }
231
+ }
232
+ }
233
+ return entries;
234
+ }
235
+ const LOCAL_SCALAR_REF_FIELDS = new Set([
236
+ "epic",
237
+ "parent",
238
+ "prev",
239
+ "next",
240
+ "active_node",
241
+ "work_id",
242
+ "work_order_id",
243
+ "receipt_id",
244
+ "target_id",
245
+ ]);
246
+ const LOCAL_LIST_REF_FIELDS = new Set([
247
+ "relates",
248
+ "blocked_by",
249
+ "blocks",
250
+ "scope_refs",
251
+ "work_contracts",
252
+ "subagent_refs",
253
+ "evidence_refs",
254
+ "refs",
255
+ "scope",
256
+ ]);
257
+ const ARCHIVE_REF_LIST_FIELDS = new Set([
258
+ "artifacts",
259
+ "refs",
260
+ "links",
261
+ "input_refs",
262
+ "constraint_refs",
263
+ "proof_refs",
264
+ "attestation_refs",
265
+ "evidence_refs",
266
+ ]);
267
+ const PORTABLE_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
268
+ function normalizeGraphRef(value, sourceWorkspace, knownWorkspaces, externalWorkspaces) {
269
+ const normalized = value.toLowerCase();
270
+ if (normalized.includes("://") || normalized.startsWith("sha256:")) {
271
+ return undefined;
272
+ }
273
+ if (normalized.includes(":")) {
274
+ const [workspace, id] = normalized.split(":", 2);
275
+ if (!workspace || !id || !PORTABLE_ID_RE.test(id)) {
276
+ return undefined;
277
+ }
278
+ if (!knownWorkspaces.has(workspace) && !externalWorkspaces.has(workspace)) {
279
+ return undefined;
280
+ }
281
+ return `${workspace}:${id}`;
282
+ }
283
+ if (!PORTABLE_ID_RE.test(normalized)) {
284
+ return undefined;
285
+ }
286
+ return `${sourceWorkspace}:${normalized}`;
287
+ }
288
+ function frontmatterRefEntries(index, externalWorkspaces) {
289
+ const knownWorkspaces = new Set(index.meta.workspaces);
290
+ const entries = [];
291
+ for (const node of Object.values(index.nodes).sort((a, b) => a.qid.localeCompare(b.qid))) {
292
+ for (const [field, raw] of Object.entries(node.attributes).sort(([a], [b]) => a.localeCompare(b))) {
293
+ if (typeof raw === "string") {
294
+ if (LOCAL_SCALAR_REF_FIELDS.has(field)) {
295
+ const target = normalizeGraphRef(raw, node.ws, knownWorkspaces, externalWorkspaces);
296
+ if (target) {
297
+ entries.push({
298
+ qid: node.qid,
299
+ path: node.path,
300
+ field,
301
+ value: raw,
302
+ target,
303
+ refKind: "graph",
304
+ locationKind: "frontmatter",
305
+ });
306
+ }
307
+ }
308
+ if (raw.startsWith("archive://") && ARCHIVE_REF_LIST_FIELDS.has(field)) {
309
+ entries.push({
310
+ qid: node.qid,
311
+ path: node.path,
312
+ field,
313
+ value: raw,
314
+ refKind: "archive",
315
+ locationKind: "frontmatter",
316
+ });
317
+ }
318
+ continue;
319
+ }
320
+ if (!Array.isArray(raw)) {
321
+ continue;
322
+ }
323
+ for (const [indexValue, value] of raw.entries()) {
324
+ const indexedField = `${field}[${indexValue}]`;
325
+ if (LOCAL_LIST_REF_FIELDS.has(field)) {
326
+ const target = normalizeGraphRef(value, node.ws, knownWorkspaces, externalWorkspaces);
327
+ if (target) {
328
+ entries.push({
329
+ qid: node.qid,
330
+ path: node.path,
331
+ field: indexedField,
332
+ value,
333
+ target,
334
+ refKind: "graph",
335
+ locationKind: "frontmatter",
336
+ });
337
+ }
338
+ }
339
+ if (value.startsWith("archive://") && ARCHIVE_REF_LIST_FIELDS.has(field)) {
340
+ entries.push({
341
+ qid: node.qid,
342
+ path: node.path,
343
+ field: indexedField,
344
+ value,
345
+ refKind: "archive",
346
+ locationKind: "frontmatter",
347
+ });
348
+ }
349
+ }
350
+ }
351
+ }
352
+ return entries;
353
+ }
354
+ function archiveIdsByWorkspace(index) {
355
+ const archives = {};
356
+ for (const node of Object.values(index.nodes)) {
357
+ if (node.type !== "archive") {
358
+ continue;
359
+ }
360
+ if (!archives[node.ws]) {
361
+ archives[node.ws] = new Set();
362
+ }
363
+ archives[node.ws].add(node.id);
364
+ }
365
+ return archives;
366
+ }
367
+ function resolveTargetFilter(index, target) {
368
+ if (!target) {
369
+ return {};
370
+ }
371
+ const normalized = target.toLowerCase();
372
+ if (normalized.includes(":")) {
373
+ if (index.nodes[normalized]) {
374
+ return { qids: new Set([normalized]) };
375
+ }
376
+ return {
377
+ blocked: {
378
+ id: "refs.target.001",
379
+ family: "refs",
380
+ risk: "blocked",
381
+ status: "blocked",
382
+ reason: "target_not_found",
383
+ paths: [],
384
+ refs: [normalized],
385
+ before: { target: normalized },
386
+ apply_supported: false,
387
+ },
388
+ };
389
+ }
390
+ const matches = Object.values(index.nodes)
391
+ .filter((node) => node.id === normalized)
392
+ .map((node) => node.qid)
393
+ .sort();
394
+ if (matches.length === 1) {
395
+ return { qids: new Set(matches) };
396
+ }
397
+ return {
398
+ blocked: {
399
+ id: "refs.target.001",
400
+ family: "refs",
401
+ risk: "blocked",
402
+ status: "blocked",
403
+ reason: matches.length === 0 ? "target_not_found" : "target_ambiguous",
404
+ paths: [],
405
+ refs: matches.length === 0 ? [normalized] : matches,
406
+ before: { target: normalized, matches },
407
+ apply_supported: false,
408
+ },
409
+ };
410
+ }
411
+ function readSelectedGoalState(root) {
412
+ const filePath = path_1.default.join(root, ".mdkg", "state", "selected-goal.json");
413
+ if (!fs_1.default.existsSync(filePath)) {
414
+ return undefined;
415
+ }
416
+ try {
417
+ const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
418
+ if (typeof parsed.qid === "string" &&
419
+ typeof parsed.id === "string" &&
420
+ typeof parsed.ws === "string" &&
421
+ typeof parsed.selected_at === "string") {
422
+ return {
423
+ qid: parsed.qid.toLowerCase(),
424
+ id: parsed.id.toLowerCase(),
425
+ ws: parsed.ws.toLowerCase(),
426
+ selected_at: parsed.selected_at,
427
+ };
428
+ }
429
+ return "malformed";
430
+ }
431
+ catch {
432
+ return "malformed";
433
+ }
434
+ }
435
+ function planSelectedGoalState(root, index) {
436
+ const selected = readSelectedGoalState(root);
437
+ if (!selected) {
438
+ return [];
439
+ }
440
+ const statePath = ".mdkg/state/selected-goal.json";
441
+ if (selected === "malformed") {
442
+ return [
443
+ {
444
+ id: "refs.selected-goal.001",
445
+ family: "refs",
446
+ risk: "medium",
447
+ status: "manual_review",
448
+ reason: "selected_goal_state_malformed",
449
+ paths: [statePath],
450
+ refs: [],
451
+ evidence: {
452
+ location_kind: "selected_goal_state",
453
+ state_path: statePath,
454
+ confidence: "structured",
455
+ },
456
+ before: { state_path: statePath },
457
+ after: { command_options: ["mdkg goal select <active-goal>", "mdkg goal clear"] },
458
+ command_hint: "mdkg goal select <active-goal> or mdkg goal clear",
459
+ apply_supported: false,
460
+ },
461
+ ];
462
+ }
463
+ const node = index.nodes[selected.qid];
464
+ if (!node) {
465
+ return [
466
+ {
467
+ id: "refs.selected-goal.001",
468
+ family: "refs",
469
+ risk: "medium",
470
+ status: "manual_review",
471
+ reason: "selected_goal_missing",
472
+ paths: [statePath],
473
+ refs: [selected.qid],
474
+ evidence: {
475
+ location_kind: "selected_goal_state",
476
+ state_path: statePath,
477
+ selected_goal: selected,
478
+ confidence: "structured",
479
+ },
480
+ before: { selected_goal: selected },
481
+ after: { command_options: ["mdkg goal select <active-goal>", "mdkg goal clear"] },
482
+ command_hint: "mdkg goal select <active-goal> or mdkg goal clear",
483
+ apply_supported: false,
484
+ },
485
+ ];
486
+ }
487
+ if (node.status === "done" || node.attributes.goal_state === "achieved") {
488
+ return [
489
+ {
490
+ id: "refs.selected-goal.001",
491
+ family: "refs",
492
+ risk: "medium",
493
+ status: "manual_review",
494
+ reason: "selected_goal_achieved",
495
+ paths: [statePath],
496
+ refs: [selected.qid],
497
+ evidence: {
498
+ location_kind: "selected_goal_state",
499
+ state_path: statePath,
500
+ selected_goal: selected,
501
+ goal_status: node.status ?? null,
502
+ goal_state: node.attributes.goal_state ?? null,
503
+ confidence: "structured",
504
+ },
505
+ before: { selected_goal: selected, goal_status: node.status ?? null, goal_state: node.attributes.goal_state ?? null },
506
+ after: { command_options: ["mdkg goal select <active-goal>", "mdkg goal clear"] },
507
+ command_hint: "mdkg goal select <active-goal> or mdkg goal clear",
508
+ apply_supported: false,
509
+ },
510
+ ];
511
+ }
512
+ return [];
513
+ }
514
+ function planRefRepairs(root, target) {
515
+ const config = (0, config_1.loadConfig)(root);
516
+ const index = (0, indexer_1.buildIndex)(root, { ...config, index: { ...config.index, tolerant: true } }, { tolerant: true });
517
+ const targetFilter = resolveTargetFilter(index, target);
518
+ if (targetFilter.blocked) {
519
+ return { proposed: [], blocked: [targetFilter.blocked] };
520
+ }
521
+ const externalWorkspaces = new Set(Object.keys(config.subgraphs ?? {}));
522
+ const archiveIds = archiveIdsByWorkspace(index);
523
+ const changes = [];
524
+ const seen = new Set();
525
+ const pushChange = (change) => {
526
+ const key = `${change.reason}|${change.paths.join(",")}|${change.refs.join(",")}|${stableJson(change.before)}`;
527
+ if (seen.has(key)) {
528
+ return;
529
+ }
530
+ seen.add(key);
531
+ changes.push(change);
532
+ };
533
+ for (const entry of edgeEntries(index)) {
534
+ if (targetFilter.qids && !targetFilter.qids.has(entry.qid)) {
535
+ continue;
536
+ }
537
+ const targetNode = index.nodes[entry.target];
538
+ const [targetWorkspace] = entry.target.split(":");
539
+ if (!targetNode && targetWorkspace && externalWorkspaces.has(targetWorkspace)) {
540
+ continue;
541
+ }
542
+ if (!targetNode) {
543
+ pushChange({
544
+ id: `refs.${String(changes.length + 1).padStart(3, "0")}`,
545
+ family: "refs",
546
+ risk: "medium",
547
+ status: "manual_review",
548
+ reason: "graph_ref_missing",
549
+ paths: [entry.path],
550
+ refs: [entry.qid, entry.target],
551
+ evidence: {
552
+ location_kind: "graph_edge",
553
+ field: entry.field,
554
+ source_qid: entry.qid,
555
+ target: entry.target,
556
+ confidence: "structured",
557
+ },
558
+ before: { field: entry.field, target: entry.target, location_kind: "graph_edge" },
559
+ command_hint: `mdkg show ${entry.qid}`,
560
+ apply_supported: false,
561
+ });
562
+ continue;
563
+ }
564
+ if (["epic", "parent", "prev", "next"].includes(entry.field) &&
565
+ !index.nodes[entry.qid]?.source?.imported &&
566
+ targetNode.source?.imported) {
567
+ pushChange({
568
+ id: `refs.${String(changes.length + 1).padStart(3, "0")}`,
569
+ family: "refs",
570
+ risk: "medium",
571
+ status: "manual_review",
572
+ reason: "graph_ref_read_only_subgraph_target",
573
+ paths: [entry.path],
574
+ refs: [entry.qid, entry.target],
575
+ evidence: {
576
+ location_kind: "graph_edge",
577
+ field: entry.field,
578
+ source_qid: entry.qid,
579
+ target: entry.target,
580
+ confidence: "structured",
581
+ },
582
+ before: { field: entry.field, target: entry.target, location_kind: "graph_edge" },
583
+ command_hint: `mdkg show ${entry.qid}`,
584
+ apply_supported: false,
585
+ });
586
+ }
587
+ }
588
+ for (const entry of frontmatterRefEntries(index, externalWorkspaces)) {
589
+ if (targetFilter.qids && !targetFilter.qids.has(entry.qid)) {
590
+ continue;
591
+ }
592
+ if (entry.refKind === "graph") {
593
+ const target = entry.target;
594
+ if (!target) {
595
+ continue;
596
+ }
597
+ const [targetWorkspace] = target.split(":");
598
+ if (targetWorkspace && externalWorkspaces.has(targetWorkspace)) {
599
+ continue;
600
+ }
601
+ if (!index.nodes[target]) {
602
+ pushChange({
603
+ id: `refs.${String(changes.length + 1).padStart(3, "0")}`,
604
+ family: "refs",
605
+ risk: "medium",
606
+ status: "manual_review",
607
+ reason: "graph_ref_missing",
608
+ paths: [entry.path],
609
+ refs: [entry.qid, target],
610
+ evidence: {
611
+ location_kind: entry.locationKind,
612
+ field: entry.field,
613
+ value: entry.value,
614
+ source_qid: entry.qid,
615
+ target,
616
+ confidence: "structured",
617
+ },
618
+ before: { field: entry.field, value: entry.value, target, location_kind: entry.locationKind },
619
+ command_hint: `mdkg show ${entry.qid}`,
620
+ apply_supported: false,
621
+ });
622
+ }
623
+ continue;
624
+ }
625
+ const archiveId = (0, refs_1.archiveIdFromUri)(entry.value);
626
+ if (!archiveId || archiveIds[entry.qid.split(":", 1)[0]]?.has(archiveId)) {
627
+ continue;
628
+ }
629
+ pushChange({
630
+ id: `refs.${String(changes.length + 1).padStart(3, "0")}`,
631
+ family: "refs",
632
+ risk: "medium",
633
+ status: "manual_review",
634
+ reason: "archive_ref_missing",
635
+ paths: [entry.path],
636
+ refs: [entry.qid, entry.value],
637
+ evidence: {
638
+ location_kind: entry.locationKind,
639
+ field: entry.field,
640
+ value: entry.value,
641
+ source_qid: entry.qid,
642
+ archive_id: archiveId,
643
+ confidence: "structured",
644
+ },
645
+ before: { field: entry.field, value: entry.value, archive_id: archiveId, location_kind: entry.locationKind },
646
+ command_hint: `mdkg archive show ${entry.value}`,
647
+ apply_supported: false,
648
+ });
649
+ }
650
+ if (!target) {
651
+ for (const selectedGoalChange of planSelectedGoalState(root, index)) {
652
+ pushChange({
653
+ ...selectedGoalChange,
654
+ id: `refs.${String(changes.length + 1).padStart(3, "0")}`,
655
+ });
656
+ }
657
+ }
658
+ return { proposed: changes, blocked: [] };
659
+ }
660
+ function candidateDuplicateId(baseId, used) {
661
+ for (let index = 2;; index += 1) {
662
+ const candidate = `${baseId}-dup-${index}`;
663
+ if (!used.has(candidate)) {
664
+ used.add(candidate);
665
+ return candidate;
666
+ }
667
+ }
668
+ }
669
+ function filesContaining(root, files, needle) {
670
+ return files
671
+ .filter((filePath) => {
672
+ try {
673
+ return fs_1.default.readFileSync(filePath, "utf8").includes(needle);
674
+ }
675
+ catch {
676
+ return false;
677
+ }
678
+ })
679
+ .map((filePath) => rel(root, filePath))
680
+ .sort();
681
+ }
682
+ function countOccurrences(value, needle) {
683
+ if (needle.length === 0) {
684
+ return 0;
685
+ }
686
+ let count = 0;
687
+ let offset = 0;
688
+ for (;;) {
689
+ const index = value.indexOf(needle, offset);
690
+ if (index === -1) {
691
+ return count;
692
+ }
693
+ count += 1;
694
+ offset = index + needle.length;
695
+ }
696
+ }
697
+ function referenceRewriteItems(root, files, from, to) {
698
+ return files
699
+ .map((filePath) => {
700
+ try {
701
+ const replacementCount = countOccurrences(fs_1.default.readFileSync(filePath, "utf8"), from);
702
+ if (replacementCount === 0) {
703
+ return undefined;
704
+ }
705
+ return {
706
+ from,
707
+ to,
708
+ path: rel(root, filePath),
709
+ location_kind: "markdown_or_frontmatter_text",
710
+ confidence: "manual_review",
711
+ replacement_count: replacementCount,
712
+ };
713
+ }
714
+ catch {
715
+ return undefined;
716
+ }
717
+ })
718
+ .filter((item) => Boolean(item))
719
+ .sort((a, b) => a.path.localeCompare(b.path));
720
+ }
721
+ function planDuplicateIdRepairs(root, target) {
722
+ const config = (0, config_1.loadConfig)(root);
723
+ const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(root, config, node_1.ALLOWED_TYPES);
724
+ const docsByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
725
+ const proposed = [];
726
+ const blocked = [];
727
+ let matchedTarget = !target;
728
+ for (const alias of Object.keys(docsByAlias).sort()) {
729
+ const records = [];
730
+ const usedIds = new Set();
731
+ const files = docsByAlias[alias].sort();
732
+ for (const filePath of files) {
733
+ if (path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core") {
734
+ continue;
735
+ }
736
+ try {
737
+ const node = (0, node_1.parseNode)(fs_1.default.readFileSync(filePath, "utf8"), filePath, {
738
+ workStatusEnum: config.work.status_enum,
739
+ priorityMin: config.work.priority_min,
740
+ priorityMax: config.work.priority_max,
741
+ templateSchemas,
742
+ });
743
+ records.push({
744
+ id: node.id,
745
+ qid: `${alias}:${node.id}`,
746
+ path: rel(root, filePath),
747
+ absPath: filePath,
748
+ });
749
+ usedIds.add(node.id);
750
+ }
751
+ catch {
752
+ continue;
753
+ }
754
+ }
755
+ const groups = new Map();
756
+ for (const record of records) {
757
+ groups.set(record.id, [...(groups.get(record.id) ?? []), record]);
758
+ }
759
+ for (const [id, groupRaw] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
760
+ const group = groupRaw.sort((a, b) => a.path.localeCompare(b.path));
761
+ if (group.length < 2) {
762
+ continue;
763
+ }
764
+ const targetMatches = !target ||
765
+ target.toLowerCase() === id ||
766
+ group.some((record) => record.qid === target.toLowerCase() || record.path === target);
767
+ if (!targetMatches) {
768
+ continue;
769
+ }
770
+ matchedTarget = true;
771
+ const canonical = group[0];
772
+ const referencePaths = filesContaining(root, files, id);
773
+ const duplicateRecords = group.slice(1);
774
+ const groupPaths = group.map((record) => record.path).sort();
775
+ const deterministicRule = "keep the lexicographically first path unchanged; propose <id>-dup-<n> for each later path";
776
+ for (const duplicate of duplicateRecords) {
777
+ const candidate = candidateDuplicateId(id, usedIds);
778
+ proposed.push({
779
+ id: `ids.${String(proposed.length + 1).padStart(3, "0")}`,
780
+ family: "ids",
781
+ risk: "high",
782
+ status: "manual_review",
783
+ reason: "duplicate_id",
784
+ paths: [duplicate.path],
785
+ refs: Array.from(new Set([canonical.qid, duplicate.qid])).sort(),
786
+ evidence: {
787
+ conflict_kind: "duplicate_local_id",
788
+ branch_merge_suspected: true,
789
+ workspace: alias,
790
+ duplicate_id: id,
791
+ group_size: group.length,
792
+ group_paths: groupPaths,
793
+ canonical: {
794
+ qid: canonical.qid,
795
+ path: canonical.path,
796
+ },
797
+ duplicate: {
798
+ qid: duplicate.qid,
799
+ path: duplicate.path,
800
+ },
801
+ deterministic_rule: deterministicRule,
802
+ },
803
+ before: {
804
+ duplicate_id: id,
805
+ workspace: alias,
806
+ canonical_path: canonical.path,
807
+ duplicate_path: duplicate.path,
808
+ duplicate_group: {
809
+ canonical_path: canonical.path,
810
+ duplicate_paths: duplicateRecords.map((record) => record.path).sort(),
811
+ all_paths: groupPaths,
812
+ },
813
+ },
814
+ after: {
815
+ candidate_id: candidate,
816
+ candidate_qid: `${alias}:${candidate}`,
817
+ collision_free: true,
818
+ deterministic_rule: deterministicRule,
819
+ reference_paths: referencePaths,
820
+ reference_rewrite_plan: referenceRewriteItems(root, files, id, candidate),
821
+ },
822
+ command_hint: `review ${duplicate.path} and update id ${id} to ${candidate}`,
823
+ apply_supported: false,
824
+ });
825
+ }
826
+ }
827
+ }
828
+ if (!matchedTarget && target) {
829
+ blocked.push({
830
+ id: "ids.target.001",
831
+ family: "ids",
832
+ risk: "blocked",
833
+ status: "blocked",
834
+ reason: "target_not_found",
835
+ paths: [],
836
+ refs: [target.toLowerCase()],
837
+ before: { target: target.toLowerCase() },
838
+ apply_supported: false,
839
+ });
840
+ }
841
+ return { proposed, blocked };
842
+ }
843
+ function sortChanges(changes) {
844
+ return [...changes].sort((a, b) => {
845
+ const family = a.family.localeCompare(b.family);
846
+ if (family !== 0) {
847
+ return family;
848
+ }
849
+ const pathCompare = (a.paths[0] ?? "").localeCompare(b.paths[0] ?? "");
850
+ if (pathCompare !== 0) {
851
+ return pathCompare;
852
+ }
853
+ const id = a.id.localeCompare(b.id);
854
+ if (id !== 0) {
855
+ return id;
856
+ }
857
+ return a.reason.localeCompare(b.reason);
858
+ });
859
+ }
860
+ function riskCounts(changes) {
861
+ return {
862
+ low: changes.filter((change) => change.risk === "low").length,
863
+ medium: changes.filter((change) => change.risk === "medium").length,
864
+ high: changes.filter((change) => change.risk === "high").length,
865
+ blocked: changes.filter((change) => change.risk === "blocked").length,
866
+ };
867
+ }
868
+ function collectFixPlan(options) {
869
+ const family = normalizeFamily(options.family);
870
+ const selected = selectedFamilies(family);
871
+ const root = relativeRoot(options.root);
872
+ const indexRepairs = selected.includes("index") ? planIndexRepairs(root) : { proposed: [], blocked: [] };
873
+ const refRepairs = selected.includes("refs") ? planRefRepairs(root, options.target) : { proposed: [], blocked: [] };
874
+ const idRepairs = selected.includes("ids") ? planDuplicateIdRepairs(root, options.target) : { proposed: [], blocked: [] };
875
+ const proposedChanges = sortChanges([...indexRepairs.proposed, ...refRepairs.proposed, ...idRepairs.proposed]);
876
+ const blockedChanges = sortChanges([...indexRepairs.blocked, ...refRepairs.blocked, ...idRepairs.blocked]);
877
+ const body = {
878
+ action: "fix.plan",
879
+ schema_version: 1,
880
+ root,
881
+ family,
882
+ target: options.target ?? null,
883
+ dirty: collectDirtyState(root),
884
+ families: emptyFamilySummaries(selected).map((entry) => ({
885
+ ...entry,
886
+ proposed_count: proposedChanges.filter((change) => change.family === entry.family).length,
887
+ blocked_count: blockedChanges.filter((change) => change.family === entry.family).length,
888
+ })),
889
+ risk_counts: riskCounts([...proposedChanges, ...blockedChanges]),
890
+ proposed_changes: proposedChanges,
891
+ blocked_changes: blockedChanges,
892
+ summary: {
893
+ selected_families: selected,
894
+ proposed_count: proposedChanges.length,
895
+ blocked_count: blockedChanges.length,
896
+ apply_supported: false,
897
+ apply_deferred: true,
898
+ message: "fix apply is not available; this command is review-only and writes no files",
899
+ },
900
+ };
901
+ const planHash = sha256(body);
902
+ return {
903
+ ...body,
904
+ ok: true,
905
+ generated_at: new Date().toISOString(),
906
+ plan_hash: planHash,
907
+ plan_id: `fix-plan-${planHash.slice("sha256:".length, "sha256:".length + 16)}`,
908
+ };
909
+ }
910
+ function runFixPlanCommand(options) {
911
+ const payload = collectFixPlan(options);
912
+ if (options.json) {
913
+ console.log(JSON.stringify(payload, null, 2));
914
+ return;
915
+ }
916
+ console.log("fix plan");
917
+ console.log(`plan_id: ${payload.plan_id}`);
918
+ console.log(`plan_hash: ${payload.plan_hash}`);
919
+ console.log(`family: ${payload.family}`);
920
+ console.log(`proposed_changes: ${payload.proposed_changes.length}`);
921
+ console.log(`blocked_changes: ${payload.blocked_changes.length}`);
922
+ console.log("apply_supported: false");
923
+ console.log("note: fix apply is not available; rerun with --json for the machine-readable receipt");
924
+ }