dotmd-cli 0.28.2 → 0.29.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 +1 -1
- package/src/hud.mjs +17 -3
- package/src/validate.mjs +68 -4
package/package.json
CHANGED
package/src/hud.mjs
CHANGED
|
@@ -16,9 +16,23 @@ function previewList(items, max = MAX_PREVIEW) {
|
|
|
16
16
|
return slugs.join(', ') + more;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Statuses that count as "actionable" for a prompt are derived from config:
|
|
20
|
+
// types.prompt.context.expanded (the statuses the user wants prominently shown).
|
|
21
|
+
// Falls back to ['pending'] when no prompt type is configured (defensive default
|
|
22
|
+
// for stripped-down configs). This means a user who customizes
|
|
23
|
+
// types.prompt.statuses to add e.g. `urgent: { context: 'expanded' }` gets that
|
|
24
|
+
// status surfaced too, without needing a code change.
|
|
25
|
+
export function actionablePromptStatuses(config) {
|
|
26
|
+
const promptCtx = config.typeContextConfig?.get('prompt');
|
|
27
|
+
const expanded = promptCtx?.expanded;
|
|
28
|
+
if (Array.isArray(expanded) && expanded.length > 0) return new Set(expanded);
|
|
29
|
+
return new Set(['pending']);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findActionablePrompts(config) {
|
|
20
33
|
const roots = config.docsRoots || (config.docsRoot ? [config.docsRoot] : []);
|
|
21
34
|
const archiveDir = config.archiveDir || 'archived';
|
|
35
|
+
const actionable = actionablePromptStatuses(config);
|
|
22
36
|
const found = [];
|
|
23
37
|
const seen = new Set();
|
|
24
38
|
|
|
@@ -43,7 +57,7 @@ function findPendingPrompts(config) {
|
|
|
43
57
|
if (!frontmatter) continue;
|
|
44
58
|
const fm = parseSimpleFrontmatter(frontmatter);
|
|
45
59
|
if (asString(fm.type) !== 'prompt') continue;
|
|
46
|
-
if (asString(fm.status)
|
|
60
|
+
if (!actionable.has(asString(fm.status))) continue;
|
|
47
61
|
found.push(toRepoPath(filePath, config.repoRoot));
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -57,7 +71,7 @@ export function buildHud(config) {
|
|
|
57
71
|
const owned = Object.values(leases).filter(l => l.session === session).map(l => l.path);
|
|
58
72
|
const queued = listQueuedHandoffs(config).map(h => h.repoPath);
|
|
59
73
|
const stale = findStaleLeases(config).map(l => l.path);
|
|
60
|
-
const prompts =
|
|
74
|
+
const prompts = findActionablePrompts(config);
|
|
61
75
|
|
|
62
76
|
return { owned, queued, stale, prompts };
|
|
63
77
|
}
|
package/src/validate.mjs
CHANGED
|
@@ -5,11 +5,52 @@ import { toRepoPath } from './util.mjs';
|
|
|
5
5
|
|
|
6
6
|
const NOW = new Date();
|
|
7
7
|
|
|
8
|
+
// Type-conventional dirs are the directories where `dotmd new <type>` lands
|
|
9
|
+
// live (non-archive) docs of that type. Built-ins use `dir` ('plans'/'prompts')
|
|
10
|
+
// and `targetRoot`. In flat-array root configs (e.g. root: ['docs/plans',
|
|
11
|
+
// 'docs/prompts']), the root itself is a type-conventional dir; in default
|
|
12
|
+
// single-root configs (root: 'docs'), the type-conventional dirs are
|
|
13
|
+
// '<root>/plans' and '<root>/prompts'. This helper builds the union so the
|
|
14
|
+
// archive-drift check works for both layouts. Custom user templates with
|
|
15
|
+
// their own `dir` would extend this; we hard-code the built-in dir names.
|
|
16
|
+
const BUILTIN_TYPE_DIR_NAMES = ['plans', 'prompts'];
|
|
17
|
+
|
|
18
|
+
function liveTypeDirsForRoots(config) {
|
|
19
|
+
const set = new Set();
|
|
20
|
+
const roots = config.docsRoots || (config.docsRoot ? [config.docsRoot] : []);
|
|
21
|
+
for (const root of roots) {
|
|
22
|
+
const rootRel = path.relative(config.repoRoot, root).split(path.sep).join('/');
|
|
23
|
+
// The root itself is a live dir (covers flat-array layouts where the
|
|
24
|
+
// root IS the type-container).
|
|
25
|
+
set.add(rootRel);
|
|
26
|
+
// Each builtin type-dir joined to the root (covers single-root layouts
|
|
27
|
+
// where 'docs' contains 'docs/plans' and 'docs/prompts' subdirs).
|
|
28
|
+
for (const dirName of BUILTIN_TYPE_DIR_NAMES) {
|
|
29
|
+
// Skip if root already ends in this name (no double-nesting like
|
|
30
|
+
// 'docs/prompts/prompts').
|
|
31
|
+
if (path.basename(rootRel) === dirName) continue;
|
|
32
|
+
set.add(rootRel ? `${rootRel}/${dirName}` : dirName);
|
|
33
|
+
}
|
|
34
|
+
// User template dirs from config (extend the set with whatever live
|
|
35
|
+
// dirs custom types declare).
|
|
36
|
+
for (const tmpl of Object.values(config.raw?.templates ?? {})) {
|
|
37
|
+
if (!tmpl || typeof tmpl !== 'object') continue;
|
|
38
|
+
if (tmpl.dir && path.basename(rootRel) !== tmpl.dir) {
|
|
39
|
+
set.add(rootRel ? `${rootRel}/${tmpl.dir}` : tmpl.dir);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return set;
|
|
44
|
+
}
|
|
45
|
+
|
|
8
46
|
function isValidStatus(status, root, config, type) {
|
|
9
|
-
//
|
|
47
|
+
// When a doc declares a known type, that type's status set is authoritative.
|
|
48
|
+
// Falling through to the global union (across all types) would allow a
|
|
49
|
+
// `type: prompt` doc to carry `status: active` just because `active` is valid
|
|
50
|
+
// for plans — defeating the purpose of type-scoped vocabularies.
|
|
10
51
|
if (type) {
|
|
11
52
|
const typeSet = config.typeStatuses?.get(type);
|
|
12
|
-
if (typeSet
|
|
53
|
+
if (typeSet) return typeSet.has(status);
|
|
13
54
|
}
|
|
14
55
|
const rootSet = config.rootValidStatuses?.get(root);
|
|
15
56
|
if (rootSet) return rootSet.has(status);
|
|
@@ -27,8 +68,11 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
27
68
|
} else if (!isValidStatus(doc.status, doc.root, config, doc.type)) {
|
|
28
69
|
const typeSet = doc.type && config.typeStatuses?.get(doc.type);
|
|
29
70
|
const rootSet = config.rootValidStatuses?.get(doc.root);
|
|
30
|
-
|
|
31
|
-
|
|
71
|
+
// When the doc has a known type, scope the error hint to that type's vocab.
|
|
72
|
+
// Otherwise fall back to root-specific or global validStatuses.
|
|
73
|
+
const hint = typeSet
|
|
74
|
+
? `valid for type \`${doc.type}\`: ${[...typeSet].join(', ')}`
|
|
75
|
+
: `valid: ${[...(rootSet ?? config.validStatuses)].join(', ')}`;
|
|
32
76
|
doc.errors.push({ path: doc.path, level: 'error', message: `Unknown status \`${doc.status}\`; ${hint}.` });
|
|
33
77
|
}
|
|
34
78
|
|
|
@@ -95,6 +139,26 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
95
139
|
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Archived plan missing `## Closeout` section.' });
|
|
96
140
|
}
|
|
97
141
|
|
|
142
|
+
// Archive drift: a doc with an archive-flagged status (`status: archived` by
|
|
143
|
+
// default) whose parent dir is a "live" type-conventional location is
|
|
144
|
+
// misplaced — `dotmd archive` would have moved it under `<that>/archiveDir/`.
|
|
145
|
+
// Without this check, default `dotmd plans` / `dotmd prompts` views silently
|
|
146
|
+
// drop the file (because they exclude archived paths), and the user gets no
|
|
147
|
+
// signal it exists but is invisible. Nested intentional content (e.g.,
|
|
148
|
+
// `docs/plans/audit/<file>.md`) is in a non-conventional subdir and exempt.
|
|
149
|
+
if (config.lifecycle.archiveStatuses.has(doc.status)) {
|
|
150
|
+
const parentDir = path.dirname(doc.path);
|
|
151
|
+
const liveDirs = liveTypeDirsForRoots(config);
|
|
152
|
+
if (liveDirs.has(parentDir)) {
|
|
153
|
+
const expected = `${parentDir}/${config.archiveDir}/${path.basename(doc.path)}`;
|
|
154
|
+
doc.errors.push({
|
|
155
|
+
path: doc.path,
|
|
156
|
+
level: 'error',
|
|
157
|
+
message: `\`status: ${doc.status}\` but file is a direct child of \`${parentDir}/\`, not \`${parentDir}/${config.archiveDir}/\`. Run \`dotmd archive ${doc.path}\` to relocate to \`${expected}\`, or change the status.`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
98
162
|
// Validate reference fields resolve to existing files
|
|
99
163
|
const docDir = path.dirname(path.join(config.repoRoot, doc.path));
|
|
100
164
|
const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];
|