dotmd-cli 0.36.3 → 0.37.0

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/bin/dotmd.mjs CHANGED
@@ -41,7 +41,7 @@ Analyze:
41
41
 
42
42
  Validate & Fix:
43
43
  check [--fix] [--errors-only] [--json] Validate frontmatter and references
44
- doctor [--dry-run] Auto-fix everything: refs, lint, dates, index
44
+ doctor [--apply] Auto-fix everything: refs, lint, dates, index (preview by default)
45
45
  lint [--fix] Check and auto-fix frontmatter issues
46
46
  fix-refs [--dry-run] Auto-fix broken reference paths + body links
47
47
 
@@ -204,6 +204,11 @@ Options:
204
204
  --errors-only Show only errors, suppress warnings
205
205
  --fix Auto-fix broken refs, lint issues, and regenerate index
206
206
  --json Output errors and warnings as JSON
207
+ --no-collapse Show every warning per-doc (since 0.37.0, high-frequency
208
+ auto-fixable warning categories — singular module/surface
209
+ deprecations, updated-behind-git — are collapsed into a
210
+ one-line summary with the bulk-fix command). --json output
211
+ is unchanged regardless.
207
212
  --dry-run, -n Preview fixes without writing (with --fix)`,
208
213
 
209
214
  archive: `dotmd archive <file> — archive a document
@@ -342,7 +347,10 @@ Runs in sequence: fix broken references, lint --fix, sync dates from
342
347
  git, regenerate index, then show remaining issues.
343
348
 
344
349
  Modes:
345
- (default) Auto-fix pass (writes by default; honors --dry-run)
350
+ (default) Auto-fix pass previews by default since 0.37.0
351
+ (F4). Use --apply (alias --yes) to actually write;
352
+ explicit --dry-run still wins over --apply if both
353
+ are passed (safety prevails).
346
354
  --statuses Read-only diagnostic: detect overloaded status
347
355
  buckets where one status holds plans pursuing
348
356
  multiple distinct unstuck-actions. Suggests how
@@ -374,7 +382,9 @@ Modes:
374
382
  in their Version History would be misleading).
375
383
  --migrate-template --json Machine-readable result.
376
384
 
377
- Use --dry-run (-n) to preview all changes without writing anything.`,
385
+ --apply (or --yes) opts into writes for the default auto-fix pass.
386
+ Sub-modes (--statuses, --migrate-*) keep their existing contracts:
387
+ they write by default and honor --dry-run.`,
378
388
 
379
389
  'fix-refs': `dotmd fix-refs — auto-fix broken reference paths
380
390
 
@@ -871,7 +881,22 @@ async function main() {
871
881
  if (command === 'rename') { const { runRename } = await import('../src/rename.mjs'); await runRename(restArgs, config, { dryRun }); return; }
872
882
  if (command === 'migrate') { const { runMigrate } = await import('../src/migrate.mjs'); runMigrate(restArgs, config, { dryRun }); return; }
873
883
  if (command === 'fix-refs') { const { runFixRefs } = await import('../src/fix-refs.mjs'); runFixRefs(restArgs, config, { dryRun }); return; }
874
- if (command === 'doctor') { const { runDoctor } = await import('../src/doctor.mjs'); runDoctor(restArgs, config, { dryRun }); return; }
884
+ if (command === 'doctor') {
885
+ // 0.37.0 (F4): the default auto-fix loop previews by default; --apply
886
+ // (alias --yes) writes. Explicit --dry-run still works and wins over
887
+ // --apply (safety prevails). The F4 flip applies ONLY to the default
888
+ // auto-fix path — sub-modes (--statuses, --migrate-template,
889
+ // --migrate-prompts) keep their existing "write unless --dry-run"
890
+ // contract because they're explicit one-shots the user opted into.
891
+ const subMode = args.includes('--statuses') || args.includes('--migrate-template') || args.includes('--migrate-prompts');
892
+ const explicitApply = args.includes('--apply') || args.includes('--yes');
893
+ const explicitDryRun = args.includes('--dry-run') || args.includes('-n');
894
+ const doctorDryRun = subMode ? dryRun : (explicitDryRun || !explicitApply);
895
+ const filtered = restArgs.filter(a => a !== '--apply' && a !== '--yes');
896
+ const { runDoctor } = await import('../src/doctor.mjs');
897
+ runDoctor(filtered, config, { dryRun: doctorDryRun });
898
+ return;
899
+ }
875
900
  if (command === 'statuses') { const { runStatuses } = await import('../src/statuses.mjs'); await runStatuses(restArgs, config, { dryRun, type: typeArg }); return; }
876
901
 
877
902
  // All remaining commands need the index + render modules
@@ -931,6 +956,7 @@ async function main() {
931
956
  if (command === 'check') {
932
957
  const fix = args.includes('--fix');
933
958
  const errorsOnly = args.includes('--errors-only');
959
+ const noCollapse = args.includes('--no-collapse');
934
960
 
935
961
  if (fix) {
936
962
  // Auto-fix: broken refs, then lint, then rebuild index
@@ -961,7 +987,7 @@ async function main() {
961
987
  passed: freshIndex.errors.length === 0,
962
988
  }, null, 2) + '\n');
963
989
  } else {
964
- process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly }));
990
+ process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly, noCollapse }));
965
991
  }
966
992
  if (freshIndex.errors.length > 0) process.exitCode = 1;
967
993
  return;
@@ -980,7 +1006,7 @@ async function main() {
980
1006
  return;
981
1007
  }
982
1008
 
983
- process.stdout.write(renderCheck(index, config, { errorsOnly }));
1009
+ process.stdout.write(renderCheck(index, config, { errorsOnly, noCollapse }));
984
1010
  if (index.errors.length > 0) process.exitCode = 1;
985
1011
  return;
986
1012
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.36.3",
3
+ "version": "0.37.0",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,75 @@
1
+ // F13 (0.37.0): collapse high-frequency auto-fixable `dotmd check` warnings
2
+ // into one-line remediation hints. Without this, bulk-fixable noise (43
3
+ // `updated behind git history`, N singular-key deprecations) drowns out
4
+ // structural findings in the per-doc list — and forces the reader to
5
+ // reconstruct the fix command per category.
6
+ //
7
+ // Each category names the message regex to match, a short label for the
8
+ // summary line, and the exact `dotmd …` command that bulk-fixes it.
9
+
10
+ const COLLAPSE_THRESHOLD = 3;
11
+
12
+ const CATEGORIES = [
13
+ {
14
+ key: 'updated-behind-git',
15
+ match: /^frontmatter `updated:.*` is behind git history/,
16
+ label: 'docs have `updated` behind git history',
17
+ fix: 'dotmd touch --git',
18
+ },
19
+ {
20
+ key: 'singular-module',
21
+ match: /^`module:` \(singular\) is deprecated/,
22
+ label: 'docs use deprecated singular `module:`',
23
+ fix: 'dotmd lint --fix',
24
+ },
25
+ {
26
+ key: 'singular-surface',
27
+ match: /^`surface:` \(singular\) is deprecated/,
28
+ label: 'docs use deprecated singular `surface:`',
29
+ fix: 'dotmd lint --fix',
30
+ },
31
+ ];
32
+
33
+ function categoryFor(message) {
34
+ for (const cat of CATEGORIES) {
35
+ if (cat.match.test(message)) return cat;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ // Split warnings into per-doc passthrough lines and collapsed summary buckets.
41
+ // A category that hits ≥COLLAPSE_THRESHOLD warnings is summarized; below the
42
+ // threshold each warning falls through as a normal per-doc line (small counts
43
+ // don't gain from collapse and lose path information).
44
+ export function categorizeWarnings(warnings) {
45
+ const buckets = new Map();
46
+ const orphans = [];
47
+
48
+ for (const w of warnings) {
49
+ const cat = categoryFor(w.message);
50
+ if (!cat) {
51
+ orphans.push(w);
52
+ continue;
53
+ }
54
+ if (!buckets.has(cat.key)) buckets.set(cat.key, { cat, items: [] });
55
+ buckets.get(cat.key).items.push(w);
56
+ }
57
+
58
+ const collapsed = [];
59
+ const passthrough = [...orphans];
60
+
61
+ for (const { cat, items } of buckets.values()) {
62
+ if (items.length >= COLLAPSE_THRESHOLD) {
63
+ collapsed.push({ key: cat.key, label: cat.label, fix: cat.fix, count: items.length });
64
+ } else {
65
+ passthrough.push(...items);
66
+ }
67
+ }
68
+
69
+ passthrough.sort((a, b) => a.path.localeCompare(b.path));
70
+ collapsed.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
71
+
72
+ return { passthrough, collapsed };
73
+ }
74
+
75
+ export const _internalForTest = { COLLAPSE_THRESHOLD, CATEGORIES };
package/src/doctor.mjs CHANGED
@@ -53,7 +53,12 @@ export function runDoctor(argv, config, opts = {}) {
53
53
  }
54
54
 
55
55
  const { dryRun } = opts;
56
- process.stdout.write(bold('dotmd doctor') + '\n\n');
56
+ // 0.37.0 (F4): the mode banner makes it impossible to mistake a preview run
57
+ // for a real one — and tells the user the exact flag that flips it.
58
+ const modeNote = dryRun
59
+ ? dim('[preview — run with --apply to write]')
60
+ : dim('[applying changes]');
61
+ process.stdout.write(bold('dotmd doctor') + ' ' + modeNote + '\n\n');
57
62
 
58
63
  // Step 1: Fix broken references
59
64
  process.stdout.write(bold('1. Fixing broken references...') + '\n');
package/src/render.mjs CHANGED
@@ -5,6 +5,7 @@ import { extractFrontmatter } from './frontmatter.mjs';
5
5
  import { summarizeDocBody } from './ai.mjs';
6
6
  import { bold, red, yellow, green, dim } from './color.mjs';
7
7
  import { findStaleLeases } from './lease.mjs';
8
+ import { categorizeWarnings } from './check-collapse.mjs';
8
9
 
9
10
  // Render `currentState` with an `(auto)` prefix when the value was body-scraped
10
11
  // rather than read from frontmatter. Lets a reader see at a glance which docs
@@ -376,7 +377,7 @@ export function renderCheck(index, config, opts = {}) {
376
377
  }
377
378
 
378
379
  function _renderCheck(index, opts = {}) {
379
- const { errorsOnly } = opts;
380
+ const { errorsOnly, noCollapse } = opts;
380
381
  const lines = ['Check', ''];
381
382
  lines.push(`- docs scanned: ${index.docs.length}`);
382
383
  lines.push(`- errors: ${index.errors.length}`);
@@ -393,8 +394,18 @@ function _renderCheck(index, opts = {}) {
393
394
 
394
395
  if (!errorsOnly && index.warnings.length > 0) {
395
396
  lines.push(yellow('Warnings'));
396
- for (const issue of index.warnings) {
397
- lines.push(`- ${issue.path}: ${issue.message}`);
397
+ if (noCollapse) {
398
+ for (const issue of index.warnings) {
399
+ lines.push(`- ${issue.path}: ${issue.message}`);
400
+ }
401
+ } else {
402
+ const { passthrough, collapsed } = categorizeWarnings(index.warnings);
403
+ for (const issue of passthrough) {
404
+ lines.push(`- ${issue.path}: ${issue.message}`);
405
+ }
406
+ for (const sum of collapsed) {
407
+ lines.push(`- ${sum.count} ${sum.label} — run \`${sum.fix}\` to bulk-fix`);
408
+ }
398
409
  }
399
410
  lines.push('');
400
411
  }