dotmd-cli 0.32.0 → 0.32.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
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/graph.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { toSlug, toRepoPath, warn } from './util.mjs';
2
+ import { toSlug, toRepoPath, warn, resolveRefPath } from './util.mjs';
3
3
  import { bold, red, green, dim } from './color.mjs';
4
4
 
5
5
  const STATUS_COLORS = {
@@ -60,7 +60,7 @@ export function buildGraph(index, config, filters = {}) {
60
60
 
61
61
  for (const field of allRefFields) {
62
62
  for (const relPath of (doc.refFields[field] || [])) {
63
- const resolved = path.resolve(docDir, relPath);
63
+ const resolved = resolveRefPath(relPath, docDir, config.repoRoot) ?? path.resolve(docDir, relPath);
64
64
  const targetPath = toRepoPath(resolved, config.repoRoot);
65
65
  const edgeKey = `${doc.path}|${targetPath}|${field}`;
66
66
  if (edgeKeys.has(edgeKey)) continue;
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, escapeRegex, nowIso } from './util.mjs';
4
+ import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso } 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';
@@ -717,12 +717,17 @@ function updateRefsFromMovedFile(oldPath, newPath, config) {
717
717
  let raw = readFileSync(newPath, 'utf8');
718
718
  const { frontmatter, body } = extractFrontmatter(raw);
719
719
 
720
- // Fix frontmatter ref fields (YAML list items like - ./path.md)
720
+ // Fix frontmatter ref fields (YAML list items like - ./path.md).
721
+ // Resolve doc-relative first, then repo-root-relative — so a ref like
722
+ // `docs/foo/bar.md` written from any nesting level gets rewritten correctly
723
+ // when the source moves. Without the repo-root fallback, repo-relative refs
724
+ // silently skipped rewriting (existsSync on the doubled doc-relative path
725
+ // returned false).
721
726
  let newFm = frontmatter;
722
727
  const refRegex = /^(\s+-\s+)(\S+\.md)$/gm;
723
728
  newFm = newFm.replace(refRegex, (match, prefix, refPath) => {
724
- const absTarget = path.resolve(oldDir, refPath);
725
- if (!existsSync(absTarget)) return match;
729
+ const absTarget = resolveRefPath(refPath, oldDir, config.repoRoot);
730
+ if (!absTarget) return match;
726
731
  const newRelPath = path.relative(newDir, absTarget).split(path.sep).join('/');
727
732
  return `${prefix}${newRelPath}`;
728
733
  });
@@ -732,8 +737,8 @@ function updateRefsFromMovedFile(oldPath, newPath, config) {
732
737
  const linkRegex = /(\[[^\]]*\]\()([^)]+\.md)(\))/g;
733
738
  newBody = newBody.replace(linkRegex, (match, pre, href, post) => {
734
739
  if (href.startsWith('http')) return match;
735
- const absTarget = path.resolve(oldDir, href);
736
- if (!existsSync(absTarget)) return match;
740
+ const absTarget = resolveRefPath(href, oldDir, config.repoRoot);
741
+ if (!absTarget) return match;
737
742
  const newHref = path.relative(newDir, absTarget).split(path.sep).join('/');
738
743
  return `${pre}${newHref}${post}`;
739
744
  });
package/src/validate.mjs CHANGED
@@ -106,7 +106,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
106
106
  doc.errors.push({ path: doc.path, level: 'error', message: '`module` is required for active/ready/planned/blocked docs; use a real module, `platform`, or `none`. Accepts singular `module:` or plural `modules:` list.' });
107
107
  }
108
108
 
109
- if (config.validSurfaces) {
109
+ if (config.validSurfaces && !config.lifecycle.skipWarningsFor.has(doc.status)) {
110
110
  for (const surface of doc.surfaces) {
111
111
  if (!config.validSurfaces.has(surface)) {
112
112
  doc.warnings.push({ path: doc.path, level: 'warning', message: `Unknown surface \`${surface}\`; expected a known surface taxonomy value.` });
@@ -164,21 +164,28 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
164
164
  }
165
165
  }
166
166
 
167
- // Validate reference fields resolve to existing files
167
+ // Validate reference fields resolve to existing files. Terminal statuses
168
+ // (archived, deprecated, etc.) document historical state — their refs may
169
+ // legitimately point at moved/deleted targets and shouldn't gate the
170
+ // exit code with a hard error.
168
171
  const docDir = path.dirname(path.join(config.repoRoot, doc.path));
169
172
  const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];
170
- for (const field of allRefFields) {
171
- for (const relPath of (doc.refFields[field] || [])) {
172
- if (!resolveRefPath(relPath, docDir, config.repoRoot)) {
173
- doc.errors.push({ path: doc.path, level: 'error', message: `${field} entry \`${relPath}\` does not resolve to an existing file.` });
173
+ const skipRefValidation = config.lifecycle.terminalStatuses.has(doc.status)
174
+ || config.lifecycle.skipWarningsFor.has(doc.status);
175
+ if (!skipRefValidation) {
176
+ for (const field of allRefFields) {
177
+ for (const relPath of (doc.refFields[field] || [])) {
178
+ if (!resolveRefPath(relPath, docDir, config.repoRoot)) {
179
+ doc.errors.push({ path: doc.path, level: 'error', message: `${field} entry \`${relPath}\` does not resolve to an existing file.` });
180
+ }
174
181
  }
175
182
  }
176
- }
177
183
 
178
- // Validate body links resolve to existing files
179
- for (const link of (doc.bodyLinks || [])) {
180
- if (!resolveRefPath(link.href, docDir, config.repoRoot)) {
181
- doc.warnings.push({ path: doc.path, level: 'warning', message: `body link \`${link.href}\` does not resolve to an existing file.` });
184
+ // Validate body links resolve to existing files
185
+ for (const link of (doc.bodyLinks || [])) {
186
+ if (!resolveRefPath(link.href, docDir, config.repoRoot)) {
187
+ doc.warnings.push({ path: doc.path, level: 'warning', message: `body link \`${link.href}\` does not resolve to an existing file.` });
188
+ }
182
189
  }
183
190
  }
184
191
  }
@@ -271,19 +278,24 @@ export function validatePlanShape(doc, body, frontmatter, config) {
271
278
  });
272
279
  }
273
280
 
274
- // 3. surface AND surfaces both populated
275
- if (frontmatter.surface && Array.isArray(frontmatter.surfaces) && frontmatter.surfaces.length > 0) {
281
+ // 3. surface AND surfaces both populated with DIVERGENT values. When the
282
+ // singular value is already a member of the plural array, src/index.mjs
283
+ // merges them transparently — warning would be noise. Only divergence
284
+ // actually risks data loss when readers consult one form vs. the other.
285
+ if (frontmatter.surface && Array.isArray(frontmatter.surfaces) && frontmatter.surfaces.length > 0
286
+ && !frontmatter.surfaces.includes(frontmatter.surface)) {
276
287
  doc.warnings.push({
277
288
  path: doc.path,
278
289
  level: 'warning',
279
- message: 'Both `surface` (singular) and `surfaces` (array) are set. Pick one — prefer `surfaces` array form.',
290
+ message: `Both \`surface\` (singular: \`${frontmatter.surface}\`) and \`surfaces\` (array) are set with different values. Pick one — prefer \`surfaces\` array form.`,
280
291
  });
281
292
  }
282
- if (frontmatter.module && Array.isArray(frontmatter.modules) && frontmatter.modules.length > 0) {
293
+ if (frontmatter.module && Array.isArray(frontmatter.modules) && frontmatter.modules.length > 0
294
+ && !frontmatter.modules.includes(frontmatter.module)) {
283
295
  doc.warnings.push({
284
296
  path: doc.path,
285
297
  level: 'warning',
286
- message: 'Both `module` (singular) and `modules` (array) are set. Pick one — prefer `modules` array form.',
298
+ message: `Both \`module\` (singular: \`${frontmatter.module}\`) and \`modules\` (array) are set with different values. Pick one — prefer \`modules\` array form.`,
287
299
  });
288
300
  }
289
301