dotmd-cli 0.76.3 → 0.76.4

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
@@ -1831,7 +1831,7 @@ async function main() {
1831
1831
 
1832
1832
  // All remaining commands need the index + render modules
1833
1833
  const { buildIndex } = await import('../src/index.mjs');
1834
- const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } = await import('../src/render.mjs');
1834
+ const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage, buildReferenceValidationCoverage } = await import('../src/render.mjs');
1835
1835
  const { runFocus, runQuery } = await import('../src/query.mjs');
1836
1836
  // `dotmd check` is the one shared-buildIndex command that should auto-heal a
1837
1837
  // drifted index block (frontmatter edits by direct Edit/Write, `lint --fix`,
@@ -1898,6 +1898,7 @@ async function main() {
1898
1898
  warnings: errorsOnly ? [] : checkIndex.warnings,
1899
1899
  errorCount: checkIndex.errors.length,
1900
1900
  warningCount: checkIndex.warnings.length,
1901
+ referenceValidation: buildReferenceValidationCoverage(checkIndex, config),
1901
1902
  passed: complete ? builtInPassed : null,
1902
1903
  ...(complete ? {} : {
1903
1904
  builtInPassed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.76.3",
3
+ "version": "0.76.4",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/config.mjs CHANGED
@@ -154,6 +154,7 @@ function normalizeRichStatuses(config, userConfig) {
154
154
  // reads them has to be too.
155
155
  skipWarningsForByType: {},
156
156
  terminalStatuses: [],
157
+ terminalStatusesByType: {},
157
158
  moduleRequiredFor: [],
158
159
  // F15: status-name → directory-name (defaults to the status name verbatim).
159
160
  // Filed statuses move docs into <root>/<dirName>/ on transition INTO the
@@ -177,6 +178,7 @@ function normalizeRichStatuses(config, userConfig) {
177
178
  // declared its statuses and suppresses nothing", which must not fall back to
178
179
  // the global union. Absent entirely means "type never declared rich statuses".
179
180
  derived.skipWarningsForByType[typeName] ??= [];
181
+ derived.terminalStatusesByType[typeName] ??= [];
180
182
  const statusNames = [];
181
183
  const typeContext = { expanded: [], listed: [], counted: [] };
182
184
  const typeStaleDays = {};
@@ -218,7 +220,10 @@ function normalizeRichStatuses(config, userConfig) {
218
220
  if (!derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
219
221
  (derived.skipWarningsForByType[typeName] ??= []).push(name);
220
222
  }
221
- if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
223
+ if (p.terminal) {
224
+ if (!derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
225
+ derived.terminalStatusesByType[typeName].push(name);
226
+ }
222
227
  if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
223
228
  if (p.filed && !derived.filedStatuses[name]) {
224
229
  // dirName defaults to the status name; users can override with
@@ -281,6 +286,10 @@ function applyDerivedConfig(config, userConfig, derived) {
281
286
  }
282
287
  if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
283
288
  config.lifecycle.terminalStatuses = derived.terminalStatuses;
289
+ config.lifecycle.terminalStatusesByType = derived.terminalStatusesByType;
290
+ } else {
291
+ // An explicit lifecycle list is intentionally repo-wide.
292
+ config.lifecycle.terminalStatusesByType = {};
284
293
  }
285
294
  if (!userConfig.lifecycle?.filedStatuses && Object.keys(derived.filedStatuses).length) {
286
295
  config.lifecycle.filedStatuses = derived.filedStatuses;
@@ -539,6 +548,13 @@ export async function resolveConfig(cwd, explicitConfigPath) {
539
548
  return own ? own.has(status) : skipWarningsFor.has(status);
540
549
  };
541
550
  const terminalStatuses = new Set(lifecycle.terminalStatuses);
551
+ const terminalStatusesByType = new Map(
552
+ Object.entries(lifecycle.terminalStatusesByType ?? {}).map(([type, names]) => [type, new Set(names)]),
553
+ );
554
+ const isTerminal = (status, type) => {
555
+ const own = type != null ? terminalStatusesByType.get(type) : undefined;
556
+ return own ? own.has(status) : terminalStatuses.has(status);
557
+ };
542
558
  // F15: filedStatuses keyed by status name, value = directory name. Empty
543
559
  // object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
544
560
  const filedStatuses = new Map(Object.entries(lifecycle.filedStatuses ?? {}));
@@ -589,7 +605,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
589
605
  rootValidStatuses,
590
606
  staleDaysByStatus,
591
607
 
592
- lifecycle: { archiveStatuses, startableStatuses, skipStaleFor, skipWarningsFor, skipWarningsForByType, skipsWarnings, terminalStatuses, filedStatuses, archiveNestedTypes },
608
+ lifecycle: { archiveStatuses, startableStatuses, skipStaleFor, skipWarningsFor, skipWarningsForByType, skipsWarnings, terminalStatuses, terminalStatusesByType, isTerminal, filedStatuses, archiveNestedTypes },
593
609
 
594
610
  validSurfaces,
595
611
  validModules,
package/src/index.mjs CHANGED
@@ -290,7 +290,8 @@ export function parseDocFile(filePath, config, opts = {}) {
290
290
  // criterion: "should defer to frontmatter when status is terminal."
291
291
  const fmCurrentState = asString(parsedFrontmatter.current_state);
292
292
  const docStatus = asString(parsedFrontmatter.status);
293
- const isTerminalDoc = docStatus && config.lifecycle?.terminalStatuses?.has?.(docStatus);
293
+ const isTerminalDoc = docStatus && (config.lifecycle?.isTerminal?.(docStatus, asString(parsedFrontmatter.type) ?? null)
294
+ ?? config.lifecycle?.terminalStatuses?.has?.(docStatus));
294
295
  // Track where currentState came from so renderers can prefix `(auto)` on
295
296
  // body-scraped values. Frontmatter wins silently; body-scraped values flag
296
297
  // their origin so the user knows the string was inferred (and that adding
package/src/render.mjs CHANGED
@@ -404,7 +404,7 @@ export function renderBriefing(index, config) {
404
404
  }
405
405
 
406
406
  export function renderCheck(index, config, opts = {}) {
407
- const defaultRenderer = (idx) => _renderCheck(idx, opts);
407
+ const defaultRenderer = (idx) => _renderCheck(idx, config, opts);
408
408
  if (!config._execution?.suppressSideEffects && config.hooks.renderCheck) {
409
409
  try { return config.hooks.renderCheck(index, defaultRenderer); }
410
410
  catch (err) { warn(`Hook 'renderCheck' threw: ${err.message}`); }
@@ -500,12 +500,26 @@ export function renderManualFixes(index) {
500
500
  return lines.join('\n');
501
501
  }
502
502
 
503
- function _renderCheck(index, opts = {}) {
503
+ export function buildReferenceValidationCoverage(index, config) {
504
+ let checkedDocs = 0;
505
+ let terminalDocsSkipped = 0;
506
+ for (const doc of index.docs) {
507
+ const terminal = config.lifecycle.isTerminal?.(doc.status, doc.type)
508
+ ?? config.lifecycle.terminalStatuses.has(doc.status);
509
+ if (terminal) terminalDocsSkipped++;
510
+ else checkedDocs++;
511
+ }
512
+ return { checkedDocs, terminalDocsSkipped };
513
+ }
514
+
515
+ function _renderCheck(index, config, opts = {}) {
504
516
  const { errorsOnly, noCollapse, verbose } = opts;
517
+ const referenceValidation = buildReferenceValidationCoverage(index, config);
505
518
  const lines = ['Check', ''];
506
519
  lines.push(`- docs scanned: ${index.docs.length}`);
507
520
  lines.push(`- errors: ${index.errors.length}`);
508
521
  lines.push(`- warnings: ${index.warnings.length}`);
522
+ lines.push(`- reference validation: ${referenceValidation.checkedDocs} docs checked; ${referenceValidation.terminalDocsSkipped} terminal docs skipped`);
509
523
  lines.push('');
510
524
 
511
525
  if (index.errors.length > 0) {
@@ -30,7 +30,7 @@ export function resolveStatusMetadata(config) {
30
30
  context: contextByStatus.get(name) ?? 'counted',
31
31
  staleDays: hasTypeStaleDays ? typeDef.staleDays[name] : (config.staleDaysByStatus?.[name] ?? null),
32
32
  startable: config.lifecycle.startableStatuses.has(name),
33
- terminal: config.lifecycle.terminalStatuses.has(name),
33
+ terminal: config.lifecycle.isTerminal?.(name, type) ?? config.lifecycle.terminalStatuses.has(name),
34
34
  archive: config.lifecycle.archiveStatuses.has(name),
35
35
  filed: config.lifecycle.filedStatuses.get(name) ?? null,
36
36
  skipStale,
package/src/validate.mjs CHANGED
@@ -259,18 +259,20 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
259
259
  // exit code with a hard error.
260
260
  const docDir = path.dirname(path.join(config.repoRoot, doc.path));
261
261
  const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];
262
- const skipRefValidation = config.lifecycle.terminalStatuses.has(doc.status)
263
- || config.lifecycle.skipsWarnings(doc.status, doc.type);
262
+ const skipRefValidation = config.lifecycle.isTerminal?.(doc.status, doc.type)
263
+ ?? config.lifecycle.terminalStatuses.has(doc.status);
264
264
  if (!skipRefValidation) {
265
+ const compatibilityWarning = config.lifecycle.skipsWarnings(doc.status, doc.type);
265
266
  for (const field of allRefFields) {
266
267
  for (const relPath of (doc.refFields[field] || [])) {
267
268
  if (!resolveRefPath(relPath, docDir, config.repoRoot)) {
268
- doc.errors.push({
269
+ const issue = {
269
270
  path: doc.path,
270
- level: 'error',
271
+ level: compatibilityWarning ? 'warning' : 'error',
271
272
  message: `${field} entry \`${relPath}\` does not resolve to an existing file.`,
272
- meta: { kind: 'ref-resolution', field, relPath },
273
- });
273
+ meta: { kind: compatibilityWarning ? 'ref-resolution-compat' : 'ref-resolution', field, relPath },
274
+ };
275
+ (compatibilityWarning ? doc.warnings : doc.errors).push(issue);
274
276
  }
275
277
  }
276
278
  }
@@ -326,7 +328,7 @@ function candidatePathsForType(docs, type) {
326
328
  // name implies one (e.g. `related_plans` → plans only).
327
329
  export function enrichRefErrorSuggestions(docs, config) {
328
330
  const enrich = (entry) => {
329
- if (!entry?.meta || entry.meta.kind !== 'ref-resolution') return;
331
+ if (!entry?.meta || !['ref-resolution', 'ref-resolution-compat'].includes(entry.meta.kind)) return;
330
332
  if (entry._suggested) return;
331
333
  const inferred = inferRefFieldType(entry.meta.field);
332
334
  const candidates = candidatePathsForType(docs, inferred);