dotmd-cli 0.47.0 → 0.48.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/config.mjs +31 -1
- package/src/lifecycle.mjs +42 -1
- package/src/validate.mjs +17 -2
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -58,10 +58,19 @@ const DEFAULTS = {
|
|
|
58
58
|
skipStaleFor: ['archived', 'reference', 'partial', 'queued-after'],
|
|
59
59
|
skipWarningsFor: ['archived', 'partial', 'queued-after'],
|
|
60
60
|
terminalStatuses: ['archived', 'deprecated', 'reference'],
|
|
61
|
+
// F15: opt-in per-status `{ filed: true }` in `types.<type>.statuses`
|
|
62
|
+
// populates this map (status → dirName). Empty by default — archive: true
|
|
63
|
+
// remains a separate primitive untouched.
|
|
64
|
+
filedStatuses: {},
|
|
61
65
|
},
|
|
62
66
|
|
|
63
67
|
taxonomy: {
|
|
64
68
|
surfaces: null,
|
|
69
|
+
// modules: when null (default), the project doesn't enumerate product
|
|
70
|
+
// modules — the modules-required validator silently skips. When set to
|
|
71
|
+
// an array, modules are taxonomy-enforced AND moduleRequiredFor activates.
|
|
72
|
+
// Mirrors the long-standing semantics of `taxonomy.surfaces`.
|
|
73
|
+
modules: null,
|
|
65
74
|
moduleRequiredFor: ['partial', 'paused', 'awaiting', 'queued-after'],
|
|
66
75
|
},
|
|
67
76
|
|
|
@@ -126,6 +135,12 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
126
135
|
skipWarningsFor: [],
|
|
127
136
|
terminalStatuses: [],
|
|
128
137
|
moduleRequiredFor: [],
|
|
138
|
+
// F15: status-name → directory-name (defaults to the status name verbatim).
|
|
139
|
+
// Filed statuses move docs into <root>/<dirName>/ on transition INTO the
|
|
140
|
+
// status, and back to flat <root>/ on transition OUT. Separate from
|
|
141
|
+
// archive: true to avoid touching that long-standing primitive's
|
|
142
|
+
// semantics.
|
|
143
|
+
filedStatuses: {},
|
|
129
144
|
staleDays: {},
|
|
130
145
|
statusOrder: [],
|
|
131
146
|
context: { expanded: [], listed: [], counted: [] },
|
|
@@ -177,6 +192,11 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
177
192
|
if ((p.skipWarnings || quietImpliesSkipWarnings) && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
178
193
|
if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
|
|
179
194
|
if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
|
|
195
|
+
if (p.filed && !derived.filedStatuses[name]) {
|
|
196
|
+
// dirName defaults to the status name; users can override with
|
|
197
|
+
// `filed: 'custom-dir'` (string form) instead of `filed: true`.
|
|
198
|
+
derived.filedStatuses[name] = typeof p.filed === 'string' ? p.filed : name;
|
|
199
|
+
}
|
|
180
200
|
|
|
181
201
|
if (!derived.statusOrder.includes(name)) derived.statusOrder.push(name);
|
|
182
202
|
}
|
|
@@ -222,6 +242,9 @@ function applyDerivedConfig(config, userConfig, derived) {
|
|
|
222
242
|
if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
|
|
223
243
|
config.lifecycle.terminalStatuses = derived.terminalStatuses;
|
|
224
244
|
}
|
|
245
|
+
if (!userConfig.lifecycle?.filedStatuses && Object.keys(derived.filedStatuses).length) {
|
|
246
|
+
config.lifecycle.filedStatuses = derived.filedStatuses;
|
|
247
|
+
}
|
|
225
248
|
|
|
226
249
|
// taxonomy.moduleRequiredFor
|
|
227
250
|
if (!userConfig.taxonomy?.moduleRequiredFor && derived.moduleRequiredFor.length) {
|
|
@@ -426,6 +449,9 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
426
449
|
const validSurfaces = config.taxonomy.surfaces
|
|
427
450
|
? new Set(config.taxonomy.surfaces)
|
|
428
451
|
: null;
|
|
452
|
+
const validModules = config.taxonomy.modules
|
|
453
|
+
? new Set(config.taxonomy.modules)
|
|
454
|
+
: null;
|
|
429
455
|
const moduleRequiredStatuses = new Set(config.taxonomy.moduleRequiredFor);
|
|
430
456
|
|
|
431
457
|
const indexPath = config.index?.path
|
|
@@ -442,6 +468,9 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
442
468
|
const skipStaleFor = new Set(lifecycle.skipStaleFor);
|
|
443
469
|
const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
|
|
444
470
|
const terminalStatuses = new Set(lifecycle.terminalStatuses);
|
|
471
|
+
// F15: filedStatuses keyed by status name, value = directory name. Empty
|
|
472
|
+
// object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
|
|
473
|
+
const filedStatuses = new Map(Object.entries(lifecycle.filedStatuses ?? {}));
|
|
445
474
|
|
|
446
475
|
// Warn if rootStatuses keys don't match any configured root
|
|
447
476
|
for (const rootKey of Object.keys(rootStatusesRaw)) {
|
|
@@ -473,9 +502,10 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
473
502
|
rootValidStatuses,
|
|
474
503
|
staleDaysByStatus,
|
|
475
504
|
|
|
476
|
-
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses },
|
|
505
|
+
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses },
|
|
477
506
|
|
|
478
507
|
validSurfaces,
|
|
508
|
+
validModules,
|
|
479
509
|
moduleRequiredStatuses,
|
|
480
510
|
|
|
481
511
|
indexPath,
|
package/src/lifecycle.mjs
CHANGED
|
@@ -168,9 +168,22 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
168
168
|
const today = nowIso();
|
|
169
169
|
const archiveDir = path.join(fileRoot, config.archiveDir);
|
|
170
170
|
const relFromRoot = path.relative(fileRoot, filePath);
|
|
171
|
+
const relSegments = relFromRoot.split(path.sep);
|
|
171
172
|
const inArchive = relFromRoot.startsWith(config.archiveDir + '/') || relFromRoot.startsWith(config.archiveDir + path.sep);
|
|
172
173
|
const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !inArchive;
|
|
173
174
|
const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && inArchive;
|
|
175
|
+
|
|
176
|
+
// F15 filing: a status with `filed: true` lives in `<root>/<dirName>/`. The
|
|
177
|
+
// current parent dir under root tells us whether the file is in some
|
|
178
|
+
// "bucket" right now. Archiving keeps its own path; filing is a separate
|
|
179
|
+
// primitive that fires only when the new status is filed (and isn't an
|
|
180
|
+
// archive transition — archive wins by being earlier in the conditional).
|
|
181
|
+
const filedStatuses = config.lifecycle.filedStatuses ?? new Map();
|
|
182
|
+
const newFiledDir = filedStatuses.get(newStatus) ?? null;
|
|
183
|
+
const oldFiledDir = oldStatus ? (filedStatuses.get(oldStatus) ?? null) : null;
|
|
184
|
+
const currentBucket = relSegments.length > 1 ? relSegments[0] : null;
|
|
185
|
+
const isFiling = !isArchiving && !isUnarchiving && newFiledDir && currentBucket !== newFiledDir;
|
|
186
|
+
const isUnfiling = !isArchiving && !isUnarchiving && !newFiledDir && oldFiledDir && currentBucket === oldFiledDir;
|
|
174
187
|
let finalPath = filePath;
|
|
175
188
|
|
|
176
189
|
if (dryRun) {
|
|
@@ -186,7 +199,17 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
186
199
|
process.stdout.write(`${prefix} Would move: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
187
200
|
finalPath = targetPath;
|
|
188
201
|
}
|
|
189
|
-
if (
|
|
202
|
+
if (isFiling) {
|
|
203
|
+
const targetPath = path.join(fileRoot, newFiledDir, path.basename(filePath));
|
|
204
|
+
process.stdout.write(`${prefix} Would file: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
205
|
+
finalPath = targetPath;
|
|
206
|
+
}
|
|
207
|
+
if (isUnfiling) {
|
|
208
|
+
const targetPath = path.join(fileRoot, path.basename(filePath));
|
|
209
|
+
process.stdout.write(`${prefix} Would unfile: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
210
|
+
finalPath = targetPath;
|
|
211
|
+
}
|
|
212
|
+
if ((isArchiving || isUnarchiving || isFiling || isUnfiling) && config.indexPath) {
|
|
190
213
|
process.stdout.write(`${prefix} Would regenerate index\n`);
|
|
191
214
|
}
|
|
192
215
|
process.stdout.write(`${prefix} ${toRepoPath(finalPath, config.repoRoot)}: ${oldStatus ?? 'unknown'} → ${newStatus}\n`);
|
|
@@ -212,6 +235,24 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
212
235
|
finalPath = targetPath;
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
if (isFiling) {
|
|
239
|
+
const targetDir = path.join(fileRoot, newFiledDir);
|
|
240
|
+
mkdirSync(targetDir, { recursive: true });
|
|
241
|
+
const targetPath = path.join(targetDir, path.basename(filePath));
|
|
242
|
+
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
243
|
+
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
244
|
+
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
245
|
+
finalPath = targetPath;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isUnfiling) {
|
|
249
|
+
const targetPath = path.join(fileRoot, path.basename(filePath));
|
|
250
|
+
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
251
|
+
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
252
|
+
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
253
|
+
finalPath = targetPath;
|
|
254
|
+
}
|
|
255
|
+
|
|
215
256
|
// Regen the index on every status change — `active → planned` etc. drift
|
|
216
257
|
// the per-status sections just as much as archive crossings. Archive paths
|
|
217
258
|
// also benefit (replaces the previously-gated regen). `--no-index` skips
|
package/src/validate.mjs
CHANGED
|
@@ -19,6 +19,11 @@ const BUILTIN_TYPE_DIR_NAMES = ['plans', 'prompts'];
|
|
|
19
19
|
function liveTypeDirsForRoots(config) {
|
|
20
20
|
const set = new Set();
|
|
21
21
|
const roots = config.docsRoots || (config.docsRoot ? [config.docsRoot] : []);
|
|
22
|
+
// F15: filed-status bucket dirs are "live" too — a doc whose status is
|
|
23
|
+
// an archive status but whose parent dir is a filed bucket should still
|
|
24
|
+
// trigger the archive-drift warning (the file needs to move into the
|
|
25
|
+
// archive bucket).
|
|
26
|
+
const filedDirs = [...((config.lifecycle?.filedStatuses?.values?.()) ?? [])];
|
|
22
27
|
for (const root of roots) {
|
|
23
28
|
const rootRel = path.relative(config.repoRoot, root).split(path.sep).join('/');
|
|
24
29
|
// The root itself is a live dir (covers flat-array layouts where the
|
|
@@ -40,6 +45,11 @@ function liveTypeDirsForRoots(config) {
|
|
|
40
45
|
set.add(rootRel ? `${rootRel}/${tmpl.dir}` : tmpl.dir);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
48
|
+
// F15: filed bucket dirs joined to each root.
|
|
49
|
+
for (const dirName of filedDirs) {
|
|
50
|
+
if (path.basename(rootRel) === dirName) continue;
|
|
51
|
+
set.add(rootRel ? `${rootRel}/${dirName}` : dirName);
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
return set;
|
|
45
55
|
}
|
|
@@ -107,8 +117,13 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
107
117
|
doc.errors.push({ path: doc.path, level: 'error', message: '`modules` must be a YAML list when present.' });
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
// modules-required gate: only fires when the project has actively declared
|
|
121
|
+
// a module taxonomy (`taxonomy.modules` is an array). Repos with no product
|
|
122
|
+
// modules (CLIs, dev tooling, single-domain apps) shouldn't be forced to
|
|
123
|
+
// sprinkle `modules: [none]` on every plan as a sentinel. Mirrors the
|
|
124
|
+
// long-standing `taxonomy.surfaces` opt-in semantics.
|
|
125
|
+
if (config.validModules && config.moduleRequiredStatuses.has(doc.status) && !doc.modules?.length) {
|
|
126
|
+
doc.errors.push({ path: doc.path, level: 'error', message: '`modules` is required for this status; declare a real module from `taxonomy.modules`, or `none` as the explicit no-module sentinel.' });
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
if (config.validSurfaces && !config.lifecycle.skipWarningsFor.has(doc.status)) {
|