dotmd-cli 0.14.11 → 0.14.12

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.14.11",
3
+ "version": "0.14.12",
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",
@@ -22,11 +22,18 @@ export function replaceFrontmatter(raw, newFrontmatter) {
22
22
  return `---\n${newFrontmatter}\n---\n${body}`;
23
23
  }
24
24
 
25
- export function parseSimpleFrontmatter(text) {
25
+ // Parses our YAML subset. Optional `warnings` array receives non-fatal
26
+ // structural issues (e.g. duplicate keys) — caller decides whether to surface
27
+ // them. Default behavior is unchanged: keep first occurrence of a duplicate
28
+ // key, ignore subsequent ones.
29
+ export function parseSimpleFrontmatter(text, warnings) {
26
30
  const data = {};
31
+ const seenDupKeys = new Set();
27
32
  let currentArrayKey = null;
33
+ let lineNum = 0;
28
34
 
29
35
  for (const rawLine of text.split('\n')) {
36
+ lineNum++;
30
37
  const line = rawLine.replace(/\r$/, '');
31
38
  if (!line.trim()) continue;
32
39
 
@@ -35,6 +42,11 @@ export function parseSimpleFrontmatter(text) {
35
42
  const [, key, rawValue] = keyMatch;
36
43
  if (Object.prototype.hasOwnProperty.call(data, key)) {
37
44
  currentArrayKey = null;
45
+ if (warnings && !seenDupKeys.has(key)) {
46
+ seenDupKeys.add(key);
47
+ warnings.push({ key, line: lineNum,
48
+ message: `Duplicate frontmatter key \`${key}\` at line ${lineNum}; keeping first occurrence, ignoring later values.` });
49
+ }
38
50
  continue;
39
51
  }
40
52
  if (!rawValue.trim()) {
package/src/index.mjs CHANGED
@@ -120,7 +120,8 @@ export function parseDocFile(filePath, config) {
120
120
  const relativePath = toRepoPath(filePath, config.repoRoot);
121
121
  const raw = readFileSync(filePath, 'utf8');
122
122
  const { frontmatter, body } = extractFrontmatter(raw);
123
- const parsedFrontmatter = parseSimpleFrontmatter(frontmatter);
123
+ const fmWarnings = [];
124
+ const parsedFrontmatter = parseSimpleFrontmatter(frontmatter, fmWarnings);
124
125
  const headingTitle = extractFirstHeading(body);
125
126
  const title = asString(parsedFrontmatter.title) ?? headingTitle ?? path.basename(filePath, '.md');
126
127
  const summary = asString(parsedFrontmatter.summary) ?? extractSummary(body) ?? null;
@@ -187,6 +188,10 @@ export function parseDocFile(filePath, config) {
187
188
  errors: [],
188
189
  };
189
190
 
191
+ for (const w of fmWarnings) {
192
+ doc.warnings.push({ path: relativePath, level: 'warning', message: w.message });
193
+ }
194
+
190
195
  validateDoc(doc, parsedFrontmatter, headingTitle, config);
191
196
  return doc;
192
197
  }
package/src/util.mjs CHANGED
@@ -100,3 +100,17 @@ export function resolveDocPath(input, config) {
100
100
 
101
101
  return null;
102
102
  }
103
+
104
+ // Resolve a reference path written in frontmatter or a body link.
105
+ // Tries doc-relative first (the historical convention), then falls back to
106
+ // repo-root-relative — so paths like `docs/foo/bar.md` written from any nesting
107
+ // level resolve correctly. Returns the absolute path if either form exists,
108
+ // else null.
109
+ export function resolveRefPath(relPath, docDir, repoRoot) {
110
+ if (!relPath) return null;
111
+ const docRelative = path.resolve(docDir, relPath);
112
+ if (existsSync(docRelative)) return docRelative;
113
+ const repoRelative = path.resolve(repoRoot, relPath);
114
+ if (existsSync(repoRelative)) return repoRelative;
115
+ return null;
116
+ }
package/src/validate.mjs CHANGED
@@ -1,6 +1,5 @@
1
- import { existsSync } from 'node:fs';
2
1
  import path from 'node:path';
3
- import { asString } from './util.mjs';
2
+ import { asString, resolveRefPath } from './util.mjs';
4
3
  import { getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
5
4
  import { toRepoPath } from './util.mjs';
6
5
 
@@ -101,8 +100,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
101
100
  const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];
102
101
  for (const field of allRefFields) {
103
102
  for (const relPath of (doc.refFields[field] || [])) {
104
- const resolved = path.resolve(docDir, relPath);
105
- if (!existsSync(resolved)) {
103
+ if (!resolveRefPath(relPath, docDir, config.repoRoot)) {
106
104
  doc.errors.push({ path: doc.path, level: 'error', message: `${field} entry \`${relPath}\` does not resolve to an existing file.` });
107
105
  }
108
106
  }
@@ -110,8 +108,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
110
108
 
111
109
  // Validate body links resolve to existing files
112
110
  for (const link of (doc.bodyLinks || [])) {
113
- const resolved = path.resolve(docDir, link.href);
114
- if (!existsSync(resolved)) {
111
+ if (!resolveRefPath(link.href, docDir, config.repoRoot)) {
115
112
  doc.warnings.push({ path: doc.path, level: 'warning', message: `body link \`${link.href}\` does not resolve to an existing file.` });
116
113
  }
117
114
  }
@@ -128,7 +125,12 @@ export function checkBidirectionalReferences(docs, config) {
128
125
  const refs = new Set();
129
126
  for (const field of biFields) {
130
127
  for (const relPath of (doc.refFields[field] || [])) {
131
- const resolved = path.resolve(docDir, relPath);
128
+ // Use the same doc-relative-then-repo-root fallback as validateDoc so
129
+ // both styles produce identical refMap keys; otherwise an entry like
130
+ // `docs/foo.md` (repo-root style) gets keyed as
131
+ // `<doc-parent>/docs/foo.md` and never matches the target's repo path.
132
+ const resolved = resolveRefPath(relPath, docDir, config.repoRoot)
133
+ ?? path.resolve(docDir, relPath);
132
134
  refs.add(toRepoPath(resolved, config.repoRoot));
133
135
  }
134
136
  }