dotmd-cli 0.45.1 → 0.45.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.45.1",
3
+ "version": "0.45.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/lifecycle.mjs CHANGED
@@ -552,12 +552,40 @@ export function runArchive(argv, config, opts = {}) {
552
552
 
553
553
  const archiveFileRoot = findFileRoot(filePath, config);
554
554
  const relFromRoot = path.relative(archiveFileRoot, filePath);
555
- if (relFromRoot.startsWith(config.archiveDir + '/') || relFromRoot.startsWith(config.archiveDir + path.sep)) { die(`Already archived: ${toRepoPath(filePath, config.repoRoot)}`); }
555
+ // Segment-membership covers both single-root (`<root>/archived/foo.md`) and
556
+ // multi-root (`<type-root>/archived/foo.md`) layouts. The older
557
+ // startsWith-only check missed nested cases where archived/ wasn't the first
558
+ // segment under the resolved root.
559
+ const inArchiveDir = relFromRoot.split(path.sep).includes(config.archiveDir);
556
560
 
557
561
  const raw = readFileSync(filePath, 'utf8');
558
562
  const { frontmatter, body } = extractFrontmatter(raw);
559
563
  const parsed = parseSimpleFrontmatter(frontmatter);
560
564
  const oldStatus = asString(parsed.status) ?? 'unknown';
565
+
566
+ // Heal stuck frontmatter (issue #13): file is under archiveDir/ but its
567
+ // status hasn't been flipped. Flip in place; don't try to move (it's already
568
+ // archived on disk) and don't refuse — refusal leaves the drift permanent.
569
+ if (inArchiveDir) {
570
+ if (oldStatus === 'archived') {
571
+ die(`Already archived: ${toRepoPath(filePath, config.repoRoot)}`);
572
+ }
573
+ const today = nowIso();
574
+ const repoPathHeal = toRepoPath(filePath, config.repoRoot);
575
+ if (dryRun) {
576
+ const prefix = dim('[dry-run]');
577
+ out.write(`${prefix} Would heal frontmatter in place: status: ${oldStatus} → archived, updated: ${today}\n`);
578
+ out.write(`${prefix} Would skip git mv (file already under \`${config.archiveDir}/\`)\n`);
579
+ return;
580
+ }
581
+ updateFrontmatter(filePath, { status: 'archived', updated: today });
582
+ appendVersionHistory(filePath, `Archived (frontmatter healed in place from \`${oldStatus}\`).`);
583
+ if (!noIndex) regenIndex(config);
584
+ out.write(`${green('✓ Healed')}: ${repoPathHeal} (${oldStatus} → archived; file already under \`${config.archiveDir}/\`)\n`);
585
+ if (showFiles) emitFilesFooter([repoPathHeal, config.indexPath], config);
586
+ return;
587
+ }
588
+
561
589
  const closeoutAction = closeoutTemplate ? planCloseoutInjection(body) : null;
562
590
 
563
591
  const today = nowIso();
package/src/prompts.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
4
- import { asString, toRepoPath, die, resolveDocPath } from './util.mjs';
4
+ import { asString, toRepoPath, die, resolveDocPath, isArchivedPath } from './util.mjs';
5
5
  import { buildIndex } from './index.mjs';
6
6
  import { runQuery } from './query.mjs';
7
7
  import { runArchive, runStatus } from './lifecycle.mjs';
@@ -120,7 +120,11 @@ function renderPromptsVerbose(index, config, { hasStatusFlag, includeArchived })
120
120
 
121
121
  export function pendingPromptsOldestFirst(config) {
122
122
  const index = buildIndex(config);
123
- const prompts = index.docs.filter(d => d.type === 'prompt' && d.status === 'pending');
123
+ const prompts = index.docs.filter(d =>
124
+ d.type === 'prompt'
125
+ && d.status === 'pending'
126
+ && !isArchivedPath(d.path, config),
127
+ );
124
128
 
125
129
  return prompts
126
130
  .map(d => {
package/src/query.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { capitalize, toSlug, truncate, warn, suggestCandidates } from './util.mjs';
3
+ import { capitalize, toSlug, truncate, warn, suggestCandidates, isArchivedPath } from './util.mjs';
4
4
  import { renderProgressBar, formatCurrentState } from './render.mjs';
5
5
  import { computeDaysSinceUpdate, computeIsStale } from './validate.mjs';
6
6
  import { getGitLastModifiedBatch } from './git.mjs';
@@ -176,14 +176,15 @@ export function filterDocs(docs, filters, config) {
176
176
 
177
177
  if (filters.types?.length) result = result.filter(d => filters.types.includes(d.type));
178
178
  if (filters.statuses?.length) result = result.filter(d => filters.statuses.includes(d.status));
179
- // --exclude-archived strips terminal/archive statuses unless the caller
180
- // explicitly opted back in with --include-archived.
179
+ // --exclude-archived strips terminal/archive statuses AND any file physically
180
+ // located under `archiveDir/` (issue #13: status can drift out of sync with
181
+ // the file's directory; the path is the source of truth for "is archived").
181
182
  if (filters.excludeArchived && !filters.includeArchived) {
182
183
  const archived = new Set([
183
184
  ...(config.lifecycle?.archiveStatuses ?? []),
184
185
  ...(config.lifecycle?.terminalStatuses ?? []),
185
186
  ]);
186
- result = result.filter(d => !archived.has(d.status));
187
+ result = result.filter(d => !archived.has(d.status) && !isArchivedPath(d.path, config));
187
188
  }
188
189
 
189
190
  if (filters.keyword) {
package/src/util.mjs CHANGED
@@ -55,6 +55,16 @@ export function toRepoPath(absolutePath, repoRoot) {
55
55
  return path.relative(repoRoot, absolutePath).split(path.sep).join('/');
56
56
  }
57
57
 
58
+ // True when any path segment equals `config.archiveDir`. Covers both
59
+ // `docs/plans/archived/foo.md` and `docs/prompts/archived/foo.md` regardless
60
+ // of whether the layout is single-root or flat-array. Read-side commands use
61
+ // this to skip files whose frontmatter `status:` has drifted out of sync with
62
+ // their archive location (issue #13).
63
+ export function isArchivedPath(repoPath, config) {
64
+ if (!repoPath || !config?.archiveDir) return false;
65
+ return repoPath.split('/').includes(config.archiveDir);
66
+ }
67
+
58
68
  // Emit a `files: a b c` line to stderr listing every doc / index path
59
69
  // the command touched (deduped, sorted, repo-relative). Lets agents do
60
70
  // `git add` with the exact set instead of guessing. Opt-in via
package/src/validate.mjs CHANGED
@@ -218,6 +218,27 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
218
218
  }
219
219
  }
220
220
 
221
+ // Inverse archive drift (issue #13): a doc whose path is under
222
+ // `<dir>/<archiveDir>/` but whose status is NOT an archive status. Without
223
+ // this check, the file is invisible to default queries (the path is below
224
+ // an archive bucket so it's filtered out by `--exclude-archived`), yet its
225
+ // `status: pending` (or similar) still makes it surface in pending-prompt
226
+ // scans. The heal is to flip the frontmatter via `dotmd <type-or-set>
227
+ // archive`, which now restores in place rather than failing.
228
+ if (doc.status && !config.lifecycle.archiveStatuses.has(doc.status)) {
229
+ const parentSegments = path.dirname(doc.path).split('/');
230
+ if (parentSegments.includes(config.archiveDir)) {
231
+ const heal = doc.type === 'prompt'
232
+ ? `dotmd prompts archive ${doc.path}`
233
+ : `dotmd set archived ${doc.path}`;
234
+ doc.errors.push({
235
+ path: doc.path,
236
+ level: 'error',
237
+ message: `File is under \`${config.archiveDir}/\` but \`status: ${doc.status}\` is not an archive status. Run \`${heal}\` to heal the frontmatter in place, or move the file out of the archive directory.`,
238
+ });
239
+ }
240
+ }
241
+
221
242
  // Validate reference fields resolve to existing files. Terminal statuses
222
243
  // (archived, deprecated, etc.) document historical state — their refs may
223
244
  // legitimately point at moved/deleted targets and shouldn't gate the