dotmd-cli 0.36.0 → 0.36.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.36.0",
3
+ "version": "0.36.2",
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",
package/src/config.mjs CHANGED
@@ -158,6 +158,16 @@ function normalizeRichStatuses(config, userConfig) {
158
158
  const quietImpliesSkipStale = p.quiet && p.skipStale !== false;
159
159
  const quietImpliesSkipWarnings = p.quiet && p.skipWarnings !== false;
160
160
 
161
+ // Contradiction diagnostics — emit at load so dead config doesn't fail silently.
162
+ const skipStaleEffective = p.skipStale === true || quietImpliesSkipStale;
163
+ const skipWarningsEffective = p.skipWarnings === true || quietImpliesSkipWarnings;
164
+ if (skipStaleEffective && p.staleDays != null) {
165
+ warn(`dotmd config: status "${typeName}.${name}" has skipStale: true and staleDays: ${p.staleDays} — staleDays is ignored. Drop one to silence this warning.`);
166
+ }
167
+ if (skipWarningsEffective && p.requiresModule) {
168
+ warn(`dotmd config: status "${typeName}.${name}" has skipWarnings: true and requiresModule: true — the module requirement can never fire. Drop one to silence this warning.`);
169
+ }
170
+
161
171
  if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
162
172
  if ((p.skipStale || quietImpliesSkipStale) && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
163
173
  if ((p.skipWarnings || quietImpliesSkipWarnings) && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
package/src/glossary.mjs CHANGED
@@ -7,7 +7,7 @@ import { bold, dim, green, yellow } from './color.mjs';
7
7
  function parseGlossaryTable(content, sectionHeading) {
8
8
  const headingRegex = new RegExp(`^##\\s+${sectionHeading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'm');
9
9
  const match = content.match(headingRegex);
10
- if (!match) return [];
10
+ if (!match) return { found: false, entries: [] };
11
11
 
12
12
  const sectionStart = match.index + match[0].length;
13
13
  const nextHeading = content.indexOf('\n## ', sectionStart);
@@ -47,7 +47,7 @@ function parseGlossaryTable(content, sectionHeading) {
47
47
  entries.push({ term: m[1], meaning: `UI label: "${m[2]}"${m[3] ? ` (${m[3]})` : ''}`, tiers: 'schema→UI' });
48
48
  }
49
49
 
50
- return entries;
50
+ return { found: true, entries };
51
51
  }
52
52
 
53
53
  function loadGlossary(config) {
@@ -62,7 +62,8 @@ function loadGlossary(config) {
62
62
 
63
63
  const content = readFileSync(filePath, 'utf8');
64
64
  const section = glossaryConfig.section ?? 'Terminology';
65
- return parseGlossaryTable(content, section);
65
+ const result = parseGlossaryTable(content, section);
66
+ return { ...result, path: glossaryConfig.path, section };
66
67
  }
67
68
 
68
69
  function matchTerm(query, entries) {
@@ -177,9 +178,15 @@ export function runGlossary(argv, config) {
177
178
  const listAll = argv.includes('--list');
178
179
  const term = argv.find(a => !a.startsWith('-'));
179
180
 
180
- const entries = loadGlossary(config);
181
- if (!entries) die('No glossary configured. Add glossary: { path, section } to your dotmd config.');
182
- if (entries.length === 0) die('Glossary section found but no entries parsed.');
181
+ const result = loadGlossary(config);
182
+ if (!result) die('No glossary configured. Add glossary: { path, section } to your dotmd config.');
183
+ if (!result.found) {
184
+ die(`Glossary section "## ${result.section}" not found in ${result.path}. Add the section, or update glossary.section in dotmd.config.mjs.`);
185
+ }
186
+ if (result.entries.length === 0) {
187
+ die(`Glossary section "## ${result.section}" found in ${result.path} but contains no recognizable entries (expected markdown table or schema→UI bullets).`);
188
+ }
189
+ const entries = result.entries;
183
190
 
184
191
  if (json && listAll) {
185
192
  const index = buildIndex(config);
package/src/lifecycle.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
4
- import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso } from './util.mjs';
4
+ import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates } from './util.mjs';
5
5
  import { gitMv, getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
6
6
  import { buildIndex, collectDocFiles } from './index.mjs';
7
7
  import { renderIndexFile, writeIndex } from './index-file.mjs';
@@ -101,7 +101,11 @@ export async function runStatus(argv, config, opts = {}) {
101
101
  }
102
102
  }
103
103
 
104
- if (!effectiveValid.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...effectiveValid].join(', ')}`); }
104
+ if (!effectiveValid.has(newStatus)) {
105
+ const suggestions = suggestCandidates(newStatus, [...effectiveValid]);
106
+ const hint = suggestions.length ? `\nDid you mean: ${suggestions.join(', ')}?` : '';
107
+ die(`Invalid status: ${newStatus}\nValid: ${[...effectiveValid].join(', ')}${hint}`);
108
+ }
105
109
 
106
110
  const oldStatus = asString(parsedFm.status);
107
111
 
package/src/new.mjs CHANGED
@@ -69,14 +69,27 @@ ${ctx?.bodyInput?.trim() ?? ''}
69
69
  'current_state:',
70
70
  'next_step:',
71
71
  ].join('\n'),
72
- body: (t, ctx) => `
72
+ body: (t, ctx) => {
73
+ const bodyInput = ctx?.bodyInput?.trim() ?? '';
74
+ // Full-body shortcut: if the input already authors `## Section` headings,
75
+ // it's a complete plan body the user/agent wrote start-to-finish. Drop
76
+ // the scaffold's later sections to avoid duplicate empty `## Goals`,
77
+ // `## Phases`, etc. below the user's already-filled versions. A bare title
78
+ // (`# X`) at the head of the body is honored — we don't double-print the
79
+ // scaffold's title. Otherwise emit the scaffold and slot the body into
80
+ // `## Problem` as before (section-content mode).
81
+ if (/^##\s+\S/m.test(bodyInput)) {
82
+ const hasOwnTitle = /^#\s+\S/.test(bodyInput);
83
+ return hasOwnTitle ? `\n${bodyInput}\n` : `\n# ${t}\n\n${bodyInput}\n`;
84
+ }
85
+ return `
73
86
  # ${t}
74
87
 
75
88
  > One-paragraph problem statement: what this plan is for, why now.
76
89
 
77
90
  ## Problem
78
91
 
79
- ${ctx?.bodyInput?.trim() ?? ''}
92
+ ${bodyInput}
80
93
 
81
94
  ## Goals
82
95
 
@@ -128,7 +141,8 @@ Status markers (put in heading text):
128
141
  ## Closeout
129
142
 
130
143
  <!-- Filled on archive: what shipped, key commits, deferrals dispositioned. -->
131
- `,
144
+ `;
145
+ },
132
146
  },
133
147
  prompt: {
134
148
  description: 'Saved prompt to seed a future Claude session — body is required',
package/src/query.mjs CHANGED
@@ -251,7 +251,13 @@ function getDocSummary(doc, config) {
251
251
 
252
252
  function renderQueryResults(docs, filters, config) {
253
253
  process.stdout.write('Query\n\n');
254
- process.stdout.write(`- results: ${docs.length}\n`);
254
+ const total = filters._totalBeforeLimit ?? docs.length;
255
+ const truncated = !filters.all && total > docs.length;
256
+ if (truncated) {
257
+ process.stdout.write(`- results: ${docs.length} of ${total} ${dim('(use --all to see all)')}\n`);
258
+ } else {
259
+ process.stdout.write(`- results: ${docs.length}\n`);
260
+ }
255
261
  if (filters.types?.length) process.stdout.write(`- type: ${filters.types.join(', ')}\n`);
256
262
  if (filters.statuses?.length) process.stdout.write(`- status: ${filters.statuses.join(', ')}\n`);
257
263
  if (filters.keyword) process.stdout.write(`- keyword: ${filters.keyword}\n`);
@@ -381,12 +387,13 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
381
387
  // Triage view: flat, sorted by recency, tag on right.
382
388
  process.stdout.write('\n');
383
389
  renderPlanRows(docs, filters, maxWidth, { showTag: true });
384
- // Footer when the result was capped.
385
- const hidden = totalAll - totalShown;
386
- if (hidden > 0) {
387
- process.stdout.write('\n');
388
- process.stdout.write(dim(` ${hidden} more ${noun} · dotmd ${noun} --all · dotmd ${noun} status\n`));
389
- }
390
+ }
391
+
392
+ // Footer when the result was capped — emit for every view shape.
393
+ const hidden = totalAll - totalShown;
394
+ if (hidden > 0) {
395
+ process.stdout.write('\n');
396
+ process.stdout.write(dim(` ${hidden} more ${noun} · dotmd ${noun} --all · dotmd ${noun} status\n`));
390
397
  }
391
398
 
392
399
  process.stdout.write('\n');
package/src/render.mjs CHANGED
@@ -275,7 +275,13 @@ function _renderContext(index, config, opts = {}) {
275
275
 
276
276
  const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
277
277
  if (stale.length) {
278
- lines.push(`Stale: ${stale.map(d => `${toSlug(d)} (${d.daysSinceUpdate}d)`).join(', ')}`);
278
+ const cap = config.context?.staleTailLimit ?? 8;
279
+ const shown = stale.slice(0, cap).map(d => `${toSlug(d)} (${d.daysSinceUpdate}d)`).join(', ');
280
+ const overflow = stale.length - cap;
281
+ const tail = overflow > 0
282
+ ? `${shown}, …and ${overflow} more (run \`dotmd stale\` for the full list)`
283
+ : shown;
284
+ lines.push(`Stale: ${tail}`);
279
285
  } else {
280
286
  lines.push('Stale: none');
281
287
  }