dotmd-cli 0.14.2 → 0.14.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/config.mjs +116 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
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/config.mjs CHANGED
@@ -97,6 +97,118 @@ const DEFAULTS = {
97
97
  },
98
98
  };
99
99
 
100
+ const VALID_CONTEXT_VALUES = new Set(['expanded', 'listed', 'counted']);
101
+
102
+ /**
103
+ * Normalize rich status definitions (object form) into array form + derived config.
104
+ * When types.<type>.statuses is an object like:
105
+ * { 'active': { context: 'expanded', staleDays: 14, requiresModule: true }, ... }
106
+ * this extracts all behavioral properties, converts statuses to an array for
107
+ * downstream processing, and returns derived values for lifecycle/taxonomy/context.
108
+ *
109
+ * Returns null if no types use object-form statuses.
110
+ */
111
+ function normalizeRichStatuses(config, userConfig) {
112
+ const derived = {
113
+ archiveStatuses: [],
114
+ skipStaleFor: [],
115
+ skipWarningsFor: [],
116
+ terminalStatuses: [],
117
+ moduleRequiredFor: [],
118
+ staleDays: {},
119
+ statusOrder: [],
120
+ context: { expanded: [], listed: [], counted: [] },
121
+ };
122
+
123
+ let hasRich = false;
124
+
125
+ for (const [typeName, typeDef] of Object.entries(config.types ?? {})) {
126
+ if (!typeDef.statuses || Array.isArray(typeDef.statuses)) continue;
127
+ if (typeof typeDef.statuses !== 'object') continue;
128
+
129
+ hasRich = true;
130
+ const statusNames = [];
131
+ const typeContext = { expanded: [], listed: [], counted: [] };
132
+ const typeStaleDays = {};
133
+
134
+ for (const [name, props] of Object.entries(typeDef.statuses)) {
135
+ const p = props ?? {};
136
+ statusNames.push(name);
137
+
138
+ const ctx = p.context ?? 'counted';
139
+ if (typeContext[ctx]) typeContext[ctx].push(name);
140
+ // Global context: only add if not already in any bucket (first type wins)
141
+ const inGlobal = derived.context.expanded.includes(name) ||
142
+ derived.context.listed.includes(name) || derived.context.counted.includes(name);
143
+ if (!inGlobal && derived.context[ctx]) derived.context[ctx].push(name);
144
+
145
+ if (p.staleDays != null) {
146
+ typeStaleDays[name] = p.staleDays;
147
+ derived.staleDays[name] = p.staleDays;
148
+ }
149
+
150
+ if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
151
+ if (p.skipStale && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
152
+ if (p.skipWarnings && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
153
+ if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
154
+ if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
155
+
156
+ if (!derived.statusOrder.includes(name)) derived.statusOrder.push(name);
157
+ }
158
+
159
+ // Convert to array form for downstream pipeline
160
+ typeDef.statuses = statusNames;
161
+
162
+ // Derive type-level context/staleDays unless user explicitly provided them
163
+ const userTypeDef = userConfig.types?.[typeName];
164
+ if (!userTypeDef?.context) typeDef.context = typeContext;
165
+ if (!userTypeDef?.staleDays) typeDef.staleDays = typeStaleDays;
166
+ }
167
+
168
+ if (!hasRich) return null;
169
+ return derived;
170
+ }
171
+
172
+ /**
173
+ * Apply derived values from rich status definitions into the merged config.
174
+ * Explicit user config always wins over derived values.
175
+ */
176
+ function applyDerivedConfig(config, userConfig, derived) {
177
+ // statuses.order — derive from types if user didn't explicitly set
178
+ if (!userConfig.statuses?.order && derived.statusOrder.length) {
179
+ config.statuses.order = derived.statusOrder;
180
+ }
181
+
182
+ // statuses.staleDays — merge derived as base, user overrides
183
+ if (!userConfig.statuses?.staleDays && Object.keys(derived.staleDays).length) {
184
+ config.statuses.staleDays = derived.staleDays;
185
+ }
186
+
187
+ // lifecycle — each sub-key independently
188
+ if (!userConfig.lifecycle?.archiveStatuses && derived.archiveStatuses.length) {
189
+ config.lifecycle.archiveStatuses = derived.archiveStatuses;
190
+ }
191
+ if (!userConfig.lifecycle?.skipStaleFor && derived.skipStaleFor.length) {
192
+ config.lifecycle.skipStaleFor = derived.skipStaleFor;
193
+ }
194
+ if (!userConfig.lifecycle?.skipWarningsFor && derived.skipWarningsFor.length) {
195
+ config.lifecycle.skipWarningsFor = derived.skipWarningsFor;
196
+ }
197
+ if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
198
+ config.lifecycle.terminalStatuses = derived.terminalStatuses;
199
+ }
200
+
201
+ // taxonomy.moduleRequiredFor
202
+ if (!userConfig.taxonomy?.moduleRequiredFor && derived.moduleRequiredFor.length) {
203
+ config.taxonomy.moduleRequiredFor = derived.moduleRequiredFor;
204
+ }
205
+
206
+ // context — only the status-related arrays, not recentDays/recentLimit/etc
207
+ if (!userConfig.context?.expanded) config.context.expanded = derived.context.expanded;
208
+ if (!userConfig.context?.listed) config.context.listed = derived.context.listed;
209
+ if (!userConfig.context?.counted) config.context.counted = derived.context.counted;
210
+ }
211
+
100
212
  function findConfigFile(startDir) {
101
213
  let dir = path.resolve(startDir);
102
214
  const root = path.parse(dir).root;
@@ -218,6 +330,10 @@ export async function resolveConfig(cwd, explicitConfigPath) {
218
330
 
219
331
  const config = deepMerge(DEFAULTS, userConfig);
220
332
 
333
+ // Normalize rich status definitions (object form → array + derived config)
334
+ const derived = normalizeRichStatuses(config, userConfig);
335
+ if (derived) applyDerivedConfig(config, userConfig, derived);
336
+
221
337
  const rootPaths = Array.isArray(config.root) ? config.root : [config.root];
222
338
  const docsRoots = rootPaths.map(r => path.resolve(configDir, r));
223
339
  const docsRoot = docsRoots[0]; // primary root for backwards compat