dotmd-cli 0.8.5 → 0.8.7
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 +8 -5
- package/dotmd.config.example.mjs +6 -0
- package/package.json +1 -1
- package/src/config.mjs +18 -0
- package/src/index.mjs +6 -0
- package/src/lifecycle.mjs +7 -3
- package/src/render.mjs +4 -2
- package/src/stats.mjs +5 -1
- package/src/validate.mjs +8 -3
package/bin/dotmd.mjs
CHANGED
|
@@ -431,8 +431,10 @@ async function main() {
|
|
|
431
431
|
index.docs = index.docs.filter(d => d.root === rootFilter || d.root.endsWith('/' + rootFilter) || d.root.split('/').pop() === rootFilter);
|
|
432
432
|
index.errors = index.errors.filter(e => index.docs.some(d => d.path === e.path));
|
|
433
433
|
index.warnings = index.warnings.filter(w => index.docs.some(d => d.path === w.path));
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
index.countsByStatus = {};
|
|
435
|
+
for (const doc of index.docs) {
|
|
436
|
+
const s = doc.status ?? 'unknown';
|
|
437
|
+
index.countsByStatus[s] = (index.countsByStatus[s] ?? 0) + 1;
|
|
436
438
|
}
|
|
437
439
|
}
|
|
438
440
|
|
|
@@ -551,9 +553,10 @@ async function main() {
|
|
|
551
553
|
if (command === 'context') {
|
|
552
554
|
if (args.includes('--json')) {
|
|
553
555
|
const byStatus = {};
|
|
554
|
-
for (const
|
|
555
|
-
const
|
|
556
|
-
if (
|
|
556
|
+
for (const doc of index.docs) {
|
|
557
|
+
const s = doc.status ?? 'unknown';
|
|
558
|
+
if (!byStatus[s]) byStatus[s] = [];
|
|
559
|
+
byStatus[s].push(doc);
|
|
557
560
|
}
|
|
558
561
|
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
|
|
559
562
|
process.stdout.write(JSON.stringify({
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -16,6 +16,12 @@ export const excludeDirs = ['evidence'];
|
|
|
16
16
|
// Status workflow — order determines display grouping
|
|
17
17
|
export const statuses = {
|
|
18
18
|
order: ['active', 'ready', 'planned', 'research', 'blocked', 'reference', 'archived'],
|
|
19
|
+
// Additional statuses valid only in specific roots (merged with order)
|
|
20
|
+
// Useful when different doc areas track different things (e.g. plans vs module docs)
|
|
21
|
+
// rootStatuses: {
|
|
22
|
+
// 'docs/modules': ['implemented', 'partial', 'draft', 'deprecated'],
|
|
23
|
+
// 'docs/core': ['implemented', 'partial'],
|
|
24
|
+
// },
|
|
19
25
|
// Days after which a doc is considered stale (null = never stale)
|
|
20
26
|
staleDays: {
|
|
21
27
|
active: 14,
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -216,6 +216,16 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
216
216
|
staleDaysByStatus[status] = config.statuses.staleDays?.[status] ?? null;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// Per-root additional statuses (merged with global validStatuses)
|
|
220
|
+
const rootStatusesRaw = config.statuses.rootStatuses ?? {};
|
|
221
|
+
const rootLabels = new Set(rootPaths.map(r => path.relative(configDir, path.resolve(configDir, r)).split(path.sep).join('/')));
|
|
222
|
+
const rootValidStatuses = new Map();
|
|
223
|
+
for (const [rootKey, extraStatuses] of Object.entries(rootStatusesRaw)) {
|
|
224
|
+
const merged = new Set(validStatuses);
|
|
225
|
+
for (const s of extraStatuses) merged.add(s);
|
|
226
|
+
rootValidStatuses.set(rootKey, merged);
|
|
227
|
+
}
|
|
228
|
+
|
|
219
229
|
const validSurfaces = config.taxonomy.surfaces
|
|
220
230
|
? new Set(config.taxonomy.surfaces)
|
|
221
231
|
: null;
|
|
@@ -235,6 +245,13 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
235
245
|
const skipStaleFor = new Set(lifecycle.skipStaleFor);
|
|
236
246
|
const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
|
|
237
247
|
|
|
248
|
+
// Warn if rootStatuses keys don't match any configured root
|
|
249
|
+
for (const rootKey of Object.keys(rootStatusesRaw)) {
|
|
250
|
+
if (!rootLabels.has(rootKey)) {
|
|
251
|
+
earlyWarnings.push(`Config: statuses.rootStatuses key '${rootKey}' does not match any configured root.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
238
255
|
const configWarnings = [...earlyWarnings, ...validateConfig(userConfig, config, validStatuses, indexPath)];
|
|
239
256
|
|
|
240
257
|
return {
|
|
@@ -252,6 +269,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
252
269
|
|
|
253
270
|
statusOrder,
|
|
254
271
|
validStatuses,
|
|
272
|
+
rootValidStatuses,
|
|
255
273
|
staleDaysByStatus,
|
|
256
274
|
|
|
257
275
|
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor },
|
package/src/index.mjs
CHANGED
|
@@ -51,6 +51,12 @@ export function buildIndex(config) {
|
|
|
51
51
|
status,
|
|
52
52
|
transformedDocs.filter(doc => doc.status === status).length,
|
|
53
53
|
]));
|
|
54
|
+
const knownStatuses = new Set(config.statusOrder);
|
|
55
|
+
for (const doc of transformedDocs) {
|
|
56
|
+
if (doc.status && !knownStatuses.has(doc.status)) {
|
|
57
|
+
countsByStatus[doc.status] = (countsByStatus[doc.status] ?? 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
54
60
|
|
|
55
61
|
if (config.indexPath) {
|
|
56
62
|
const indexCheck = checkIndex(transformedDocs, config);
|
package/src/lifecycle.mjs
CHANGED
|
@@ -27,11 +27,16 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
27
27
|
die('Usage: dotmd status <file> <new-status>');
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
if (!config.validStatuses.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...config.validStatuses].join(', ')}`); }
|
|
31
|
-
|
|
32
30
|
const filePath = resolveDocPath(input, config);
|
|
33
31
|
if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); }
|
|
34
32
|
|
|
33
|
+
// Validate status against root-specific vocabulary
|
|
34
|
+
const fileRoot = findFileRoot(filePath, config);
|
|
35
|
+
const rootLabel = path.relative(config.repoRoot, fileRoot).split(path.sep).join('/');
|
|
36
|
+
const rootSet = config.rootValidStatuses?.get(rootLabel);
|
|
37
|
+
const effectiveValid = rootSet ?? config.validStatuses;
|
|
38
|
+
if (!effectiveValid.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...effectiveValid].join(', ')}`); }
|
|
39
|
+
|
|
35
40
|
const raw = readFileSync(filePath, 'utf8');
|
|
36
41
|
const { frontmatter } = extractFrontmatter(raw);
|
|
37
42
|
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
@@ -43,7 +48,6 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
const today = new Date().toISOString().slice(0, 10);
|
|
46
|
-
const fileRoot = findFileRoot(filePath, config);
|
|
47
51
|
const archiveDir = path.join(fileRoot, config.archiveDir);
|
|
48
52
|
const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !filePath.includes(`/${config.archiveDir}/`);
|
|
49
53
|
const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && filePath.includes(`/${config.archiveDir}/`);
|
package/src/render.mjs
CHANGED
|
@@ -124,8 +124,10 @@ function _renderContext(index, config, opts = {}) {
|
|
|
124
124
|
const ctx = config.context;
|
|
125
125
|
|
|
126
126
|
const byStatus = {};
|
|
127
|
-
for (const
|
|
128
|
-
|
|
127
|
+
for (const doc of index.docs) {
|
|
128
|
+
const s = doc.status ?? 'unknown';
|
|
129
|
+
if (!byStatus[s]) byStatus[s] = [];
|
|
130
|
+
byStatus[s].push(doc);
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
for (const status of (ctx.expanded || [])) {
|
package/src/stats.mjs
CHANGED
|
@@ -94,7 +94,11 @@ function _renderStats(stats, config) {
|
|
|
94
94
|
|
|
95
95
|
// Status
|
|
96
96
|
lines.push(bold('Status'));
|
|
97
|
-
const
|
|
97
|
+
const allStatuses = [
|
|
98
|
+
...config.statusOrder.filter(s => stats.countsByStatus[s]),
|
|
99
|
+
...Object.keys(stats.countsByStatus).filter(s => !config.statusOrder.includes(s)).sort(),
|
|
100
|
+
];
|
|
101
|
+
const statusParts = allStatuses
|
|
98
102
|
.filter(s => stats.countsByStatus[s])
|
|
99
103
|
.map(s => `${s}: ${stats.countsByStatus[s]}`);
|
|
100
104
|
lines.push(' ' + statusParts.join(' '));
|
package/src/validate.mjs
CHANGED
|
@@ -6,15 +6,20 @@ import { toRepoPath } from './util.mjs';
|
|
|
6
6
|
|
|
7
7
|
const NOW = new Date();
|
|
8
8
|
|
|
9
|
+
function isValidStatus(status, root, config) {
|
|
10
|
+
const rootSet = config.rootValidStatuses?.get(root);
|
|
11
|
+
if (rootSet) return rootSet.has(status);
|
|
12
|
+
return config.validStatuses.has(status);
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
10
16
|
if (!doc.status) {
|
|
11
17
|
doc.errors.push({ path: doc.path, level: 'error', message: 'Missing frontmatter `status`.' });
|
|
12
|
-
} else if (!
|
|
18
|
+
} else if (!isValidStatus(doc.status, doc.root, config)) {
|
|
13
19
|
doc.warnings.push({ path: doc.path, level: 'warning', message: `Unknown status \`${doc.status}\`; not in statuses.order.` });
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
const knownStatus = config.validStatuses.has(doc.status);
|
|
22
|
+
const knownStatus = isValidStatus(doc.status, doc.root, config);
|
|
18
23
|
|
|
19
24
|
if (knownStatus && !config.lifecycle.skipWarningsFor.has(doc.status) && !doc.updated) {
|
|
20
25
|
doc.errors.push({ path: doc.path, level: 'error', message: 'Missing frontmatter `updated` for non-archived doc.' });
|