mdkg 0.3.0 → 0.3.2

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.
@@ -15,10 +15,35 @@ const template_schema_1 = require("../graph/template_schema");
15
15
  const visibility_1 = require("../graph/visibility");
16
16
  const sqlite_index_1 = require("../graph/sqlite_index");
17
17
  const project_db_1 = require("../core/project_db");
18
+ const project_db_migrations_1 = require("../core/project_db_migrations");
18
19
  const errors_1 = require("../util/errors");
19
20
  const REQUIRED_NODE_MAJOR = 24;
20
21
  const REQUIRED_NODE_MINOR = 15;
21
22
  const ARCHIVE_RAW_ALLOWED_DIRS = new Set(["source"]);
23
+ const SELECTED_GOAL_STATE_PATH = path_1.default.join(".mdkg", "state", "selected-goal.json");
24
+ function makeCheck(input) {
25
+ const level = input.level ?? (input.ok ? undefined : "fail");
26
+ const effectiveLevel = level ?? "ok";
27
+ const status = !input.ok || effectiveLevel === "fail" ? "fail" : effectiveLevel === "warn" ? "warn" : "pass";
28
+ const severity = status === "fail" ? "error" : status === "warn" ? "warning" : "info";
29
+ return {
30
+ id: input.id,
31
+ name: input.name,
32
+ ok: input.ok,
33
+ level,
34
+ detail: input.detail,
35
+ status,
36
+ severity,
37
+ message: input.message ?? input.detail,
38
+ remediation: input.remediation ?? "No action required.",
39
+ refs: input.refs,
40
+ strictFail: input.strictFail,
41
+ };
42
+ }
43
+ function publicCheck(result) {
44
+ const { strictFail: _strictFail, ...publicResult } = result;
45
+ return publicResult;
46
+ }
22
47
  function parseNodeVersion(version) {
23
48
  const [majorRaw, minorRaw, patchRaw] = version.split(".");
24
49
  const major = Number.parseInt(majorRaw ?? "", 10);
@@ -33,55 +58,67 @@ function runNodeVersionCheck() {
33
58
  const nodeVersion = process.versions.node;
34
59
  const parsed = parseNodeVersion(nodeVersion);
35
60
  if (parsed === null) {
36
- return {
61
+ return makeCheck({
62
+ id: "runtime.node_version",
37
63
  name: "node-version",
38
64
  ok: false,
39
65
  detail: `unable to parse Node.js version: ${nodeVersion}`,
40
- };
66
+ remediation: `Run mdkg with Node.js >=${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}.0.`,
67
+ });
41
68
  }
42
69
  if (parsed.major < REQUIRED_NODE_MAJOR ||
43
70
  (parsed.major === REQUIRED_NODE_MAJOR && parsed.minor < REQUIRED_NODE_MINOR)) {
44
- return {
71
+ return makeCheck({
72
+ id: "runtime.node_version",
45
73
  name: "node-version",
46
74
  ok: false,
47
75
  detail: `Node.js ${nodeVersion} is unsupported (requires >=${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}.0)`,
48
- };
76
+ remediation: `Install Node.js >=${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}.0 and rerun mdkg.`,
77
+ });
49
78
  }
50
- return {
79
+ return makeCheck({
80
+ id: "runtime.node_version",
51
81
  name: "node-version",
52
82
  ok: true,
53
83
  detail: `Node.js ${nodeVersion} (ok)`,
54
- };
84
+ });
55
85
  }
56
86
  function runSqliteCheck(root, config) {
57
87
  if (!(0, sqlite_index_1.isSqliteBackend)(config)) {
58
- return {
88
+ return makeCheck({
89
+ id: "graph.sqlite_cache",
59
90
  name: "sqlite-cache",
60
91
  ok: true,
61
92
  detail: "SQLite backend disabled; JSON cache backend active",
62
- };
93
+ });
63
94
  }
64
95
  const health = (0, sqlite_index_1.sqliteHealth)(root, config);
65
96
  if (health.errors.length > 0) {
66
- return {
97
+ return makeCheck({
98
+ id: "graph.sqlite_cache",
67
99
  name: "sqlite-cache",
68
100
  ok: false,
69
101
  detail: health.errors.join("; "),
70
- };
102
+ remediation: "Run `mdkg index` to rebuild the SQLite graph cache.",
103
+ });
71
104
  }
72
105
  if (health.warnings.length > 0) {
73
- return {
106
+ return makeCheck({
107
+ id: "graph.sqlite_cache",
74
108
  name: "sqlite-cache",
75
109
  ok: true,
76
110
  level: "warn",
77
111
  detail: health.warnings.join("; "),
78
- };
112
+ remediation: "Run `mdkg index` to refresh the SQLite graph cache.",
113
+ strictFail: true,
114
+ });
79
115
  }
80
- return {
116
+ return makeCheck({
117
+ id: "graph.sqlite_cache",
81
118
  name: "sqlite-cache",
82
119
  ok: true,
83
120
  detail: `.mdkg SQLite cache ok (${health.size} bytes)`,
84
- };
121
+ });
85
122
  }
86
123
  function walkFiles(root) {
87
124
  if (!fs_1.default.existsSync(root)) {
@@ -115,26 +152,31 @@ function runArchiveStorageCheck(root) {
115
152
  .map((filePath) => path_1.default.relative(root, filePath).split(path_1.default.sep).join("/"))
116
153
  .sort();
117
154
  if (strayRaw.length === 0) {
118
- return {
155
+ return makeCheck({
156
+ id: "archive.storage",
119
157
  name: "archive-storage",
120
158
  ok: true,
121
159
  detail: ".mdkg/archive has no stray raw files outside managed source directories",
122
- };
160
+ });
123
161
  }
124
- return {
162
+ return makeCheck({
163
+ id: "archive.storage",
125
164
  name: "archive-storage",
126
165
  ok: true,
127
166
  level: "warn",
128
167
  detail: `stray uncompressed archive file(s) found without managed sidecars: ${strayRaw.join(", ")}; run \`mdkg archive add <file>\` or move raw files under a managed archive source directory`,
129
- };
168
+ remediation: "Run `mdkg archive add <file>` or move raw files under a managed archive source directory.",
169
+ refs: strayRaw,
170
+ });
130
171
  }
131
172
  function runArchiveLargeCacheCheck(root, warningBytes) {
132
173
  if (warningBytes === 0) {
133
- return {
174
+ return makeCheck({
175
+ id: "archive.large_cache",
134
176
  name: "archive-large-cache",
135
177
  ok: true,
136
178
  detail: "archive large-cache warning disabled",
137
- };
179
+ });
138
180
  }
139
181
  const archiveRoot = path_1.default.join(root, ".mdkg", "archive");
140
182
  const largeCaches = walkFiles(archiveRoot)
@@ -146,103 +188,130 @@ function runArchiveLargeCacheCheck(root, warningBytes) {
146
188
  .filter((entry) => entry.size > warningBytes)
147
189
  .sort((a, b) => a.path.localeCompare(b.path));
148
190
  if (largeCaches.length === 0) {
149
- return {
191
+ return makeCheck({
192
+ id: "archive.large_cache",
150
193
  name: "archive-large-cache",
151
194
  ok: true,
152
195
  detail: `no archive ZIP cache exceeds ${warningBytes} bytes`,
153
- };
196
+ });
154
197
  }
155
- return {
198
+ return makeCheck({
199
+ id: "archive.large_cache",
156
200
  name: "archive-large-cache",
157
201
  ok: true,
158
202
  level: "warn",
159
203
  detail: `archive ZIP cache(s) exceed ${warningBytes} bytes: ${largeCaches
160
204
  .map((entry) => `${entry.path} (${entry.size} bytes)`)
161
205
  .join(", ")}; keep large caches private or move bulky originals to repo policy-managed storage`,
162
- };
206
+ remediation: "Keep large caches private or move bulky originals to repo policy-managed storage.",
207
+ refs: largeCaches.map((entry) => entry.path),
208
+ });
163
209
  }
164
210
  function runBundleStorageCheck(root, outputDir) {
165
211
  const bundleRoot = path_1.default.resolve(root, outputDir);
166
212
  if (!fs_1.default.existsSync(bundleRoot)) {
167
- return {
213
+ return makeCheck({
214
+ id: "bundle.storage",
168
215
  name: "bundle-storage",
169
216
  ok: true,
170
217
  detail: `no bundle directory found; run \`mdkg bundle create --profile private\` when a snapshot should be tracked`,
171
- };
218
+ remediation: "Run `mdkg bundle create --profile private` when a snapshot should be tracked.",
219
+ });
172
220
  }
173
221
  const bundles = walkFiles(bundleRoot)
174
222
  .filter((filePath) => filePath.endsWith(".mdkg.zip"))
175
223
  .map((filePath) => path_1.default.relative(root, filePath).split(path_1.default.sep).join("/"))
176
224
  .sort();
177
225
  if (bundles.length === 0) {
178
- return {
226
+ return makeCheck({
227
+ id: "bundle.storage",
179
228
  name: "bundle-storage",
180
229
  ok: true,
181
230
  detail: `bundle directory has no .mdkg.zip files; run \`mdkg bundle create --profile private\` when a snapshot should be tracked`,
182
- };
231
+ remediation: "Run `mdkg bundle create --profile private` when a snapshot should be tracked.",
232
+ });
183
233
  }
184
- return {
234
+ return makeCheck({
235
+ id: "bundle.storage",
185
236
  name: "bundle-storage",
186
237
  ok: true,
187
238
  detail: `${bundles.length} bundle(s) found; run \`mdkg bundle verify <path>\` to check freshness before handoff`,
188
- };
239
+ remediation: "Run `mdkg bundle verify <path>` to check freshness before handoff.",
240
+ refs: bundles,
241
+ });
189
242
  }
190
243
  function runProjectDbRuntimePolicyCheck(root) {
191
244
  const files = (0, project_db_1.listProjectDbRuntimePolicyFiles)(root);
192
245
  if (files.length === 0) {
193
- return {
246
+ return makeCheck({
247
+ id: "db.runtime_transient_files",
194
248
  name: "project-db-runtime",
195
249
  ok: true,
196
250
  detail: "no active project DB runtime or transient files found",
197
- };
251
+ });
198
252
  }
199
- return {
253
+ return makeCheck({
254
+ id: "db.runtime_transient_files",
200
255
  name: "project-db-runtime",
201
256
  ok: true,
202
257
  level: "warn",
203
258
  detail: `active project DB runtime/transient file(s) are local-only and should not be committed: ${files.join(", ")}`,
204
- };
259
+ remediation: "Keep runtime DB and transient files ignored; commit sealed state only by explicit repo policy.",
260
+ refs: files,
261
+ });
205
262
  }
206
263
  function runSubgraphChecks(root, config) {
207
264
  const projection = (0, subgraphs_1.buildSubgraphsIndex)(root, config);
208
265
  if (projection.index.subgraphs.length === 0) {
209
266
  return [
210
- {
267
+ makeCheck({
268
+ id: "subgraph.configured_state",
211
269
  name: "subgraphs",
212
270
  ok: true,
213
271
  detail: "no subgraphs configured",
214
- },
272
+ }),
215
273
  ];
216
274
  }
217
275
  return projection.index.subgraphs.map((item) => {
218
276
  if (!item.enabled) {
219
- return {
277
+ return makeCheck({
278
+ id: "subgraph.configured_state",
220
279
  name: `subgraph:${item.alias}`,
221
280
  ok: true,
222
281
  level: "warn",
223
282
  detail: "disabled subgraph",
224
- };
283
+ remediation: `Run \`mdkg subgraph enable ${item.alias}\` if this subgraph should participate in graph views.`,
284
+ refs: [item.alias],
285
+ });
225
286
  }
226
287
  if (item.error_count > 0) {
227
- return {
288
+ return makeCheck({
289
+ id: "subgraph.configured_state",
228
290
  name: `subgraph:${item.alias}`,
229
291
  ok: false,
230
292
  detail: item.errors.join("; "),
231
- };
293
+ remediation: `Run \`mdkg subgraph verify ${item.alias} --json\` and refresh or remove the failing bundle source.`,
294
+ refs: [item.alias],
295
+ });
232
296
  }
233
297
  if (item.stale || item.warning_count > 0) {
234
- return {
298
+ return makeCheck({
299
+ id: "subgraph.configured_state",
235
300
  name: `subgraph:${item.alias}`,
236
301
  ok: true,
237
302
  level: "warn",
238
303
  detail: `subgraph is stale or has warnings; run \`mdkg subgraph verify ${item.alias}\` (${item.warnings.join("; ")})`,
239
- };
304
+ remediation: `Run \`mdkg subgraph verify ${item.alias}\` and refresh stale sources if needed.`,
305
+ refs: [item.alias],
306
+ });
240
307
  }
241
- return {
308
+ return makeCheck({
309
+ id: "subgraph.configured_state",
242
310
  name: `subgraph:${item.alias}`,
243
311
  ok: true,
244
312
  detail: `subgraph loaded from ${item.sources.map((source) => source.path).join(", ")}`,
245
- };
313
+ refs: [item.alias],
314
+ });
246
315
  });
247
316
  }
248
317
  function runVisibilityPolicyCheck(root, config, options) {
@@ -251,184 +320,399 @@ function runVisibilityPolicyCheck(root, config, options) {
251
320
  root,
252
321
  config,
253
322
  useCache: !options.noCache,
254
- allowReindex: !options.noReindex,
323
+ allowReindex: !options.noReindex && !options.strict,
255
324
  });
256
325
  const messages = (0, visibility_1.visibilityViolationMessages)((0, visibility_1.collectVisibilityViolations)(index, config));
257
326
  if (messages.length === 0) {
258
- return {
327
+ return makeCheck({
328
+ id: "visibility.policy",
259
329
  name: "visibility-policy",
260
330
  ok: true,
261
331
  detail: "public/internal records do not reference less-visible mdkg records",
262
- };
332
+ });
263
333
  }
264
- return {
334
+ return makeCheck({
335
+ id: "visibility.policy",
265
336
  name: "visibility-policy",
266
337
  ok: false,
267
338
  detail: `${messages.length} violation(s): ${messages.join("; ")}`,
268
- };
339
+ remediation: "Adjust visibility metadata or remove references from more-visible records to less-visible records.",
340
+ });
269
341
  }
270
342
  catch (err) {
271
343
  const message = err instanceof Error ? err.message : String(err);
272
- return {
344
+ return makeCheck({
345
+ id: "visibility.policy",
273
346
  name: "visibility-policy",
274
347
  ok: false,
275
348
  detail: message,
276
- };
349
+ remediation: "Run `mdkg validate --json` for graph visibility diagnostics.",
350
+ });
351
+ }
352
+ }
353
+ function readSelectedGoalState(root) {
354
+ const filePath = path_1.default.join(root, SELECTED_GOAL_STATE_PATH);
355
+ if (!fs_1.default.existsSync(filePath)) {
356
+ return {};
357
+ }
358
+ try {
359
+ const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
360
+ if (typeof parsed.qid === "string" &&
361
+ typeof parsed.id === "string" &&
362
+ typeof parsed.ws === "string" &&
363
+ typeof parsed.selected_at === "string") {
364
+ return {
365
+ state: {
366
+ qid: parsed.qid.toLowerCase(),
367
+ id: parsed.id.toLowerCase(),
368
+ ws: parsed.ws.toLowerCase(),
369
+ selected_at: parsed.selected_at,
370
+ },
371
+ };
372
+ }
373
+ return { warning: "selected goal state is malformed" };
374
+ }
375
+ catch {
376
+ return { warning: "selected goal state is unreadable" };
377
+ }
378
+ }
379
+ function runSelectedGoalChecks(root, config, options) {
380
+ const selected = readSelectedGoalState(root);
381
+ if (selected.warning) {
382
+ return [
383
+ makeCheck({
384
+ id: "goal.selected_missing",
385
+ name: "selected-goal",
386
+ ok: true,
387
+ level: "warn",
388
+ detail: selected.warning,
389
+ remediation: "Run `mdkg goal select <goal-id>` or `mdkg goal clear`.",
390
+ strictFail: true,
391
+ }),
392
+ ];
393
+ }
394
+ if (!selected.state) {
395
+ return [
396
+ makeCheck({
397
+ id: "goal.selected_missing",
398
+ name: "selected-goal",
399
+ ok: true,
400
+ detail: "no selected goal state found",
401
+ remediation: "Run `mdkg goal select <goal-id>` when pursuing a long-running goal.",
402
+ }),
403
+ ];
404
+ }
405
+ try {
406
+ const { index } = (0, index_cache_1.loadIndex)({
407
+ root,
408
+ config,
409
+ useCache: !options.noCache,
410
+ allowReindex: !options.noReindex && !options.strict,
411
+ });
412
+ const node = index.nodes[selected.state.qid];
413
+ if (!node) {
414
+ return [
415
+ makeCheck({
416
+ id: "goal.selected_missing",
417
+ name: "selected-goal",
418
+ ok: true,
419
+ level: "warn",
420
+ detail: `selected goal ${selected.state.qid} is missing from the graph`,
421
+ remediation: "Run `mdkg goal select <active-goal>` or `mdkg goal clear`.",
422
+ refs: [selected.state.qid],
423
+ strictFail: true,
424
+ }),
425
+ ];
426
+ }
427
+ const achieved = node.status === "done" || String(node.attributes.goal_state ?? "") === "achieved";
428
+ if (achieved) {
429
+ return [
430
+ makeCheck({
431
+ id: "goal.selected_achieved",
432
+ name: "selected-goal",
433
+ ok: true,
434
+ level: "warn",
435
+ detail: `selected goal ${selected.state.qid} is achieved but still current`,
436
+ remediation: "Run `mdkg goal select <active-goal>` or `mdkg goal clear`.",
437
+ refs: [selected.state.qid],
438
+ strictFail: true,
439
+ }),
440
+ ];
441
+ }
442
+ return [
443
+ makeCheck({
444
+ id: "goal.selected_achieved",
445
+ name: "selected-goal",
446
+ ok: true,
447
+ detail: `selected goal ${selected.state.qid} is active`,
448
+ refs: [selected.state.qid],
449
+ }),
450
+ ];
451
+ }
452
+ catch (err) {
453
+ const message = err instanceof Error ? err.message : String(err);
454
+ return [
455
+ makeCheck({
456
+ id: "goal.selected_missing",
457
+ name: "selected-goal",
458
+ ok: true,
459
+ level: "warn",
460
+ detail: `selected goal could not be checked: ${message}`,
461
+ remediation: "Run `mdkg index` and `mdkg goal current --json` to inspect selected-goal state.",
462
+ refs: [selected.state.qid],
463
+ strictFail: true,
464
+ }),
465
+ ];
466
+ }
467
+ }
468
+ function runProjectDbVerifyCheck(root, config) {
469
+ if (!config.db.enabled) {
470
+ return makeCheck({
471
+ id: "db.project_verify",
472
+ name: "project-db-verify",
473
+ ok: true,
474
+ detail: "project DB is disabled",
475
+ remediation: "Run `mdkg db init` only when project DB state is needed.",
476
+ });
477
+ }
478
+ const verification = (0, project_db_migrations_1.verifyProjectDb)(root, config);
479
+ if (verification.ok) {
480
+ return makeCheck({
481
+ id: "db.project_verify",
482
+ name: "project-db-verify",
483
+ ok: true,
484
+ detail: `project DB verified (${verification.database})`,
485
+ refs: [verification.database],
486
+ });
487
+ }
488
+ return makeCheck({
489
+ id: "db.project_verify",
490
+ name: "project-db-verify",
491
+ ok: true,
492
+ level: "warn",
493
+ detail: verification.errors.join("; "),
494
+ remediation: "Run `mdkg db verify --json`, then `mdkg db init` or `mdkg db migrate` as directed.",
495
+ refs: [verification.database],
496
+ strictFail: true,
497
+ });
498
+ }
499
+ function applyStrict(results, strict) {
500
+ if (!strict) {
501
+ return results;
277
502
  }
503
+ return results.map((result) => {
504
+ if (!result.strictFail || !result.ok) {
505
+ return result;
506
+ }
507
+ return {
508
+ ...result,
509
+ ok: false,
510
+ level: "fail",
511
+ status: "fail",
512
+ severity: "error",
513
+ };
514
+ });
278
515
  }
279
516
  function runDoctorCommand(options) {
280
517
  const results = [];
518
+ const strict = options.strict ?? false;
281
519
  results.push(runNodeVersionCheck());
282
520
  const configPath = path_1.default.resolve(options.root, ".mdkg", "config.json");
283
521
  if (!fs_1.default.existsSync(configPath)) {
284
- results.push({
522
+ results.push(makeCheck({
523
+ id: "repo.root_config",
285
524
  name: "config",
286
525
  ok: false,
287
526
  detail: `missing config at ${configPath}`,
288
- });
527
+ remediation: "Run from a repo root, pass `--root <path>`, or run `mdkg init`.",
528
+ refs: [path_1.default.relative(options.root, configPath).split(path_1.default.sep).join("/")],
529
+ }));
289
530
  }
290
531
  else {
291
- results.push({
532
+ results.push(makeCheck({
533
+ id: "repo.root_config",
292
534
  name: "config",
293
535
  ok: true,
294
536
  detail: `found ${configPath}`,
295
- });
537
+ refs: [path_1.default.relative(options.root, configPath).split(path_1.default.sep).join("/")],
538
+ }));
296
539
  }
297
540
  let config;
298
541
  try {
299
542
  config = (0, config_1.loadConfig)(options.root);
300
- results.push({
543
+ results.push(makeCheck({
544
+ id: "repo.root_config",
301
545
  name: "config-schema",
302
546
  ok: true,
303
547
  detail: "config schema valid",
304
- });
548
+ }));
305
549
  }
306
550
  catch (err) {
307
551
  const message = err instanceof Error ? err.message : String(err);
308
- results.push({
552
+ results.push(makeCheck({
553
+ id: "repo.root_config",
309
554
  name: "config-schema",
310
555
  ok: false,
311
556
  detail: message,
312
- });
557
+ remediation: "Fix `.mdkg/config.json` and rerun `mdkg doctor`.",
558
+ refs: [".mdkg/config.json"],
559
+ }));
313
560
  }
314
561
  if (config) {
315
562
  results.push(runArchiveStorageCheck(options.root));
316
563
  results.push(runArchiveLargeCacheCheck(options.root, config.archive.large_cache_warning_bytes));
317
564
  results.push(runBundleStorageCheck(options.root, config.bundles.output_dir));
318
565
  results.push(runProjectDbRuntimePolicyCheck(options.root));
566
+ results.push(runProjectDbVerifyCheck(options.root, config));
319
567
  results.push(runSqliteCheck(options.root, config));
320
568
  results.push(...runSubgraphChecks(options.root, config));
321
569
  results.push(runVisibilityPolicyCheck(options.root, config, options));
570
+ results.push(...runSelectedGoalChecks(options.root, config, options));
322
571
  try {
323
572
  const templateSchemaInfo = (0, template_schema_1.loadTemplateSchemasWithInfo)(options.root, config, node_1.ALLOWED_TYPES);
324
- results.push({
573
+ results.push(makeCheck({
574
+ id: "repo.templates",
325
575
  name: "templates",
326
576
  ok: true,
327
577
  detail: "template schema set loaded",
328
- });
578
+ }));
329
579
  if (templateSchemaInfo.fallbackTypes.length > 0) {
330
- results.push({
580
+ results.push(makeCheck({
581
+ id: "repo.templates",
331
582
  name: "local-templates",
332
583
  ok: true,
333
584
  level: "warn",
334
585
  detail: `missing local template schema(s) covered by bundled fallback: ${templateSchemaInfo.fallbackTypes.join(", ")}; run \`mdkg upgrade --apply\` to vendor them`,
335
- });
586
+ remediation: "Run `mdkg upgrade --apply` to vendor missing managed template schemas.",
587
+ refs: templateSchemaInfo.fallbackTypes,
588
+ }));
336
589
  }
337
590
  }
338
591
  catch (err) {
339
592
  const message = err instanceof Error ? err.message : String(err);
340
- results.push({
593
+ results.push(makeCheck({
594
+ id: "repo.templates",
341
595
  name: "templates",
342
596
  ok: false,
343
597
  detail: message,
344
- });
598
+ remediation: "Repair `.mdkg/templates` or run `mdkg upgrade --apply` when managed assets should be restored.",
599
+ }));
345
600
  }
346
601
  try {
347
602
  const { rebuilt, stale } = (0, index_cache_1.loadIndex)({
348
603
  root: options.root,
349
604
  config,
350
605
  useCache: !options.noCache,
351
- allowReindex: !options.noReindex,
606
+ allowReindex: !options.noReindex && !strict,
352
607
  });
353
608
  if (rebuilt) {
354
- results.push({
609
+ results.push(makeCheck({
610
+ id: "graph.index_cache",
355
611
  name: "index",
356
612
  ok: true,
357
613
  detail: "index cache rebuilt and loaded",
358
- });
614
+ remediation: "No action required; non-strict doctor rebuilt the cache.",
615
+ }));
359
616
  }
360
617
  else if (stale) {
361
- results.push({
618
+ results.push(makeCheck({
619
+ id: "graph.index_cache",
362
620
  name: "index",
363
621
  ok: true,
622
+ level: "warn",
364
623
  detail: "index cache is stale (run mdkg index to refresh)",
365
- });
624
+ remediation: "Run `mdkg index` to refresh generated graph caches.",
625
+ strictFail: true,
626
+ }));
366
627
  }
367
628
  else {
368
- results.push({
629
+ results.push(makeCheck({
630
+ id: "graph.index_cache",
369
631
  name: "index",
370
632
  ok: true,
371
633
  detail: "index cache loaded",
372
- });
634
+ }));
373
635
  }
374
636
  }
375
637
  catch (err) {
376
638
  const message = err instanceof Error ? err.message : String(err);
377
- results.push({
639
+ results.push(makeCheck({
640
+ id: "graph.validate",
378
641
  name: "index",
379
642
  ok: false,
380
643
  detail: message,
381
- });
644
+ remediation: "Run `mdkg validate --json` and repair graph errors, then run `mdkg index`.",
645
+ }));
382
646
  }
383
647
  try {
384
648
  const { rebuilt, stale } = (0, capabilities_index_cache_1.loadCapabilitiesIndex)({
385
649
  root: options.root,
386
650
  config,
387
651
  useCache: !options.noCache,
388
- allowReindex: !options.noReindex,
652
+ allowReindex: !options.noReindex && !strict,
389
653
  });
390
654
  if (rebuilt) {
391
- results.push({
655
+ results.push(makeCheck({
656
+ id: "graph.capability_cache",
392
657
  name: "capabilities-index",
393
658
  ok: true,
394
659
  detail: "capabilities cache rebuilt and loaded",
395
- });
660
+ remediation: "No action required; non-strict doctor rebuilt the cache.",
661
+ }));
396
662
  }
397
663
  else if (stale) {
398
- results.push({
664
+ results.push(makeCheck({
665
+ id: "graph.capability_cache",
399
666
  name: "capabilities-index",
400
667
  ok: true,
668
+ level: "warn",
401
669
  detail: "capabilities cache is stale (run mdkg index to refresh)",
402
- });
670
+ remediation: "Run `mdkg index` to refresh generated capability caches.",
671
+ strictFail: true,
672
+ }));
403
673
  }
404
674
  else {
405
- results.push({
675
+ results.push(makeCheck({
676
+ id: "graph.capability_cache",
406
677
  name: "capabilities-index",
407
678
  ok: true,
408
679
  detail: "capabilities cache loaded",
409
- });
680
+ }));
410
681
  }
411
682
  }
412
683
  catch (err) {
413
684
  const message = err instanceof Error ? err.message : String(err);
414
- results.push({
685
+ results.push(makeCheck({
686
+ id: "graph.capability_cache",
415
687
  name: "capabilities-index",
416
688
  ok: false,
417
689
  detail: message,
418
- });
690
+ remediation: "Run `mdkg index` to rebuild generated capability caches.",
691
+ }));
419
692
  }
420
693
  }
421
- const failures = results.filter((result) => !result.ok);
694
+ const finalResults = applyStrict(results, strict);
695
+ const publicResults = finalResults.map(publicCheck);
696
+ const summary = {
697
+ ok: finalResults.every((result) => result.ok),
698
+ errors: finalResults.filter((result) => result.severity === "error").length,
699
+ warnings: finalResults.filter((result) => result.severity === "warning").length,
700
+ infos: finalResults.filter((result) => result.severity === "info").length,
701
+ };
702
+ const failures = finalResults.filter((result) => !result.ok);
422
703
  if (options.json) {
423
704
  const payload = {
705
+ action: "doctor",
424
706
  ok: failures.length === 0,
425
- checks: results,
707
+ strict,
708
+ checks: publicResults,
709
+ summary,
426
710
  failure_count: failures.length,
427
711
  };
428
712
  console.log(JSON.stringify(payload, null, 2));
429
713
  }
430
714
  else {
431
- for (const result of results) {
715
+ for (const result of finalResults) {
432
716
  const prefix = result.ok ? result.level ?? "ok" : "fail";
433
717
  console.log(`${prefix}: ${result.name} - ${result.detail}`);
434
718
  }