dotmd-cli 0.74.3 → 0.74.4
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 +36 -2
- package/src/frontmatter-fix.mjs +1 -1
- package/src/hub-membership.mjs +8 -8
- package/src/lint.mjs +1 -1
- package/src/render.mjs +2 -2
- package/src/stats.mjs +1 -1
- package/src/status-metadata.mjs +1 -1
- package/src/statuses.mjs +1 -1
- package/src/sync-status.mjs +5 -5
- package/src/validate.mjs +24 -24
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -146,6 +146,13 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
146
146
|
archiveStatuses: [],
|
|
147
147
|
skipStaleFor: [],
|
|
148
148
|
skipWarningsFor: [],
|
|
149
|
+
// Same membership as `skipWarningsFor`, but kept per declaring type. The flat
|
|
150
|
+
// list is a union of status NAMES, so a name any type marks quiet suppresses
|
|
151
|
+
// warnings for every other type that happens to share it — `journey.active`
|
|
152
|
+
// silencing every `active` plan in the repo. Statuses are type-scoped
|
|
153
|
+
// everywhere else (`typeStatuses`, `isValidStatus`), so the suppression that
|
|
154
|
+
// reads them has to be too.
|
|
155
|
+
skipWarningsForByType: {},
|
|
149
156
|
terminalStatuses: [],
|
|
150
157
|
moduleRequiredFor: [],
|
|
151
158
|
// F15: status-name → directory-name (defaults to the status name verbatim).
|
|
@@ -166,6 +173,10 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
166
173
|
if (typeof typeDef.statuses !== 'object') continue;
|
|
167
174
|
|
|
168
175
|
hasRich = true;
|
|
176
|
+
// Claim the key even when no status is quiet: an empty list means "this type
|
|
177
|
+
// declared its statuses and suppresses nothing", which must not fall back to
|
|
178
|
+
// the global union. Absent entirely means "type never declared rich statuses".
|
|
179
|
+
derived.skipWarningsForByType[typeName] ??= [];
|
|
169
180
|
const statusNames = [];
|
|
170
181
|
const typeContext = { expanded: [], listed: [], counted: [] };
|
|
171
182
|
const typeStaleDays = {};
|
|
@@ -203,7 +214,10 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
203
214
|
if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
|
|
204
215
|
if (p.startable && !derived.startableStatuses.includes(name)) derived.startableStatuses.push(name);
|
|
205
216
|
if ((p.skipStale || quietImpliesSkipStale) && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
|
|
206
|
-
if (
|
|
217
|
+
if (p.skipWarnings || quietImpliesSkipWarnings) {
|
|
218
|
+
if (!derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
219
|
+
(derived.skipWarningsForByType[typeName] ??= []).push(name);
|
|
220
|
+
}
|
|
207
221
|
if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
|
|
208
222
|
if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
|
|
209
223
|
if (p.filed && !derived.filedStatuses[name]) {
|
|
@@ -253,8 +267,17 @@ function applyDerivedConfig(config, userConfig, derived) {
|
|
|
253
267
|
if (!userConfig.lifecycle?.skipStaleFor && derived.skipStaleFor.length) {
|
|
254
268
|
config.lifecycle.skipStaleFor = derived.skipStaleFor;
|
|
255
269
|
}
|
|
270
|
+
// Assigned on BOTH branches, never conditionally: `config.lifecycle` can be the
|
|
271
|
+
// module-level DEFAULTS.lifecycle object itself (deepMerge shallow-copies, so a
|
|
272
|
+
// user config with no `lifecycle` key shares the reference), and a conditional
|
|
273
|
+
// write would leave a previous resolveConfig call's map in place.
|
|
256
274
|
if (!userConfig.lifecycle?.skipWarningsFor && derived.skipWarningsFor.length) {
|
|
257
275
|
config.lifecycle.skipWarningsFor = derived.skipWarningsFor;
|
|
276
|
+
config.lifecycle.skipWarningsForByType = derived.skipWarningsForByType;
|
|
277
|
+
} else {
|
|
278
|
+
// An explicit `lifecycle.skipWarningsFor` is a deliberate repo-wide statement
|
|
279
|
+
// and stays type-blind.
|
|
280
|
+
config.lifecycle.skipWarningsForByType = {};
|
|
258
281
|
}
|
|
259
282
|
if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
|
|
260
283
|
config.lifecycle.terminalStatuses = derived.terminalStatuses;
|
|
@@ -504,6 +527,17 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
504
527
|
const startableStatuses = new Set(lifecycle.startableStatuses ?? ['active', 'planned']);
|
|
505
528
|
const skipStaleFor = new Set(lifecycle.skipStaleFor);
|
|
506
529
|
const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
|
|
530
|
+
const skipWarningsForByType = new Map(
|
|
531
|
+
Object.entries(lifecycle.skipWarningsForByType ?? {}).map(([type, names]) => [type, new Set(names)]),
|
|
532
|
+
);
|
|
533
|
+
// Does `status` suppress warning-only checks for a doc of this type? A type that
|
|
534
|
+
// declared rich statuses answers for itself — including answering "no" for a
|
|
535
|
+
// name some other type marked quiet. Everything else (no rich statuses, an
|
|
536
|
+
// explicit global `skipWarningsFor`, a doc with no type) reads the flat set.
|
|
537
|
+
const skipsWarnings = (status, type) => {
|
|
538
|
+
const own = type != null ? skipWarningsForByType.get(type) : undefined;
|
|
539
|
+
return own ? own.has(status) : skipWarningsFor.has(status);
|
|
540
|
+
};
|
|
507
541
|
const terminalStatuses = new Set(lifecycle.terminalStatuses);
|
|
508
542
|
// F15: filedStatuses keyed by status name, value = directory name. Empty
|
|
509
543
|
// object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
|
|
@@ -555,7 +589,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
555
589
|
rootValidStatuses,
|
|
556
590
|
staleDaysByStatus,
|
|
557
591
|
|
|
558
|
-
lifecycle: { archiveStatuses, startableStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses, archiveNestedTypes },
|
|
592
|
+
lifecycle: { archiveStatuses, startableStatuses, skipStaleFor, skipWarningsFor, skipWarningsForByType, skipsWarnings, terminalStatuses, filedStatuses, archiveNestedTypes },
|
|
559
593
|
|
|
560
594
|
validSurfaces,
|
|
561
595
|
validModules,
|
package/src/frontmatter-fix.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export function runFrontmatterFix(config, opts = {}) {
|
|
|
33
33
|
const status = asString(parsed.status);
|
|
34
34
|
if (config.lifecycle.terminalStatuses.has(status)
|
|
35
35
|
|| config.lifecycle.archiveStatuses.has(status)
|
|
36
|
-
|| config.lifecycle.
|
|
36
|
+
|| config.lifecycle.skipsWarnings(status, docType)) continue;
|
|
37
37
|
|
|
38
38
|
const ops = [];
|
|
39
39
|
for (const { name, cap, target, heading } of FIELDS) {
|
package/src/hub-membership.mjs
CHANGED
|
@@ -39,10 +39,10 @@ const BACKREF_KIND = 'hub-membership-backref';
|
|
|
39
39
|
|
|
40
40
|
export function checkHubMembershipDrift(docs, config) {
|
|
41
41
|
const warnings = [];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// Per-doc: warning suppression is type-scoped, so a status name quiet for one
|
|
43
|
+
// type stays loud for another that declared the same name.
|
|
44
|
+
const quiet = (d) => config.lifecycle?.terminalStatuses?.has(d.status)
|
|
45
|
+
|| config.lifecycle?.skipsWarnings(d.status, d.type);
|
|
46
46
|
const byPath = new Map(docs.map(doc => [doc.path, doc]));
|
|
47
47
|
const refFields = [
|
|
48
48
|
...(config.referenceFields?.bidirectional ?? []),
|
|
@@ -82,7 +82,7 @@ export function checkHubMembershipDrift(docs, config) {
|
|
|
82
82
|
// it lives. Warns on the HUB: the hub's list is the half that lost the entry.
|
|
83
83
|
const knownCache = new Map();
|
|
84
84
|
for (const child of docs) {
|
|
85
|
-
if (quiet
|
|
85
|
+
if (quiet(child)) continue;
|
|
86
86
|
const parents = child.refFields?.parent_plan ?? [];
|
|
87
87
|
if (parents.length === 0) continue;
|
|
88
88
|
const dir = dirOf(child);
|
|
@@ -92,7 +92,7 @@ export function checkHubMembershipDrift(docs, config) {
|
|
|
92
92
|
// parent rows nothing, and demanding a link back there would be a new
|
|
93
93
|
// opinion rather than a drift check.
|
|
94
94
|
if (!hub || hub.path === child.path || !isHubDoc(hub)) continue;
|
|
95
|
-
if (quiet
|
|
95
|
+
if (quiet(hub)) continue;
|
|
96
96
|
if (!knownCache.has(hub.path)) knownCache.set(hub.path, knownTo(hub));
|
|
97
97
|
if (knownCache.get(hub.path).has(child.path)) continue;
|
|
98
98
|
warnings.push({
|
|
@@ -113,7 +113,7 @@ export function checkHubMembershipDrift(docs, config) {
|
|
|
113
113
|
// back-ref is never reported twice. Warns on the CHILD, matching that check:
|
|
114
114
|
// it's the file that needs the edit.
|
|
115
115
|
for (const hub of docs) {
|
|
116
|
-
if (!isHubDoc(hub) || quiet
|
|
116
|
+
if (!isHubDoc(hub) || quiet(hub)) continue;
|
|
117
117
|
let body;
|
|
118
118
|
try { ({ body } = extractFrontmatter(readFileSync(path.join(config.repoRoot, hub.path), 'utf8'))); }
|
|
119
119
|
catch { continue; }
|
|
@@ -131,7 +131,7 @@ export function checkHubMembershipDrift(docs, config) {
|
|
|
131
131
|
const child = resolve(ref, dir);
|
|
132
132
|
if (!child || child.path === hub.path || seen.has(child.path)) continue;
|
|
133
133
|
seen.add(child.path);
|
|
134
|
-
if (quiet
|
|
134
|
+
if (quiet(child)) continue; // closed work is normal history
|
|
135
135
|
if (inFrontmatterRunlist.has(child.path)) continue; // checkRunlistBackPointers owns it
|
|
136
136
|
if (isHubDoc(child)) continue; // a hub under a hub is the roadmap tier
|
|
137
137
|
if (child.type && child.type !== 'plan') continue; // `parent_plan` is a plan relationship
|
package/src/lint.mjs
CHANGED
|
@@ -79,7 +79,7 @@ export function runLint(argv, config, opts = {}) {
|
|
|
79
79
|
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
80
80
|
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
81
81
|
const fixes = [];
|
|
82
|
-
const skipWarnings = config.lifecycle.
|
|
82
|
+
const skipWarnings = config.lifecycle.skipsWarnings(asString(parsed.status), asString(parsed.type));
|
|
83
83
|
|
|
84
84
|
// Missing type (fixable — infer from root: plans → 'plan', else 'doc')
|
|
85
85
|
if (!asString(parsed.type)) {
|
package/src/render.mjs
CHANGED
|
@@ -291,8 +291,8 @@ function _renderContext(index, config, opts = {}) {
|
|
|
291
291
|
lines.push('Stale: none');
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
const withErrors = index.docs.filter(d => d.errors.length > 0 && !config.lifecycle.
|
|
295
|
-
const withWarnings = index.docs.filter(d => d.warnings.length > 0 && !config.lifecycle.
|
|
294
|
+
const withErrors = index.docs.filter(d => d.errors.length > 0 && !config.lifecycle.skipsWarnings(d.status, d.type));
|
|
295
|
+
const withWarnings = index.docs.filter(d => d.warnings.length > 0 && !config.lifecycle.skipsWarnings(d.status, d.type));
|
|
296
296
|
if (withErrors.length || withWarnings.length) {
|
|
297
297
|
const parts = [];
|
|
298
298
|
if (withErrors.length) parts.push(`${withErrors.length} with errors`);
|
package/src/stats.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export function buildStats(index, config) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
const scoped = docs.filter(d => scope.includes(d.status));
|
|
18
|
-
const nonArchived = docs.filter(d => !config.lifecycle.
|
|
18
|
+
const nonArchived = docs.filter(d => !config.lifecycle.skipsWarnings(d.status, d.type));
|
|
19
19
|
|
|
20
20
|
// Health
|
|
21
21
|
const staleCount = nonArchived.filter(d => d.isStale).length;
|
package/src/status-metadata.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export function resolveStatusMetadata(config) {
|
|
|
22
22
|
|
|
23
23
|
byType[type] = statuses.map((name, rank) => {
|
|
24
24
|
const skipStale = config.lifecycle.skipStaleFor.has(name);
|
|
25
|
-
const skipWarnings = config.lifecycle.
|
|
25
|
+
const skipWarnings = config.lifecycle.skipsWarnings(name, type);
|
|
26
26
|
const hasTypeStaleDays = Object.prototype.hasOwnProperty.call(typeDef.staleDays ?? {}, name);
|
|
27
27
|
return {
|
|
28
28
|
name,
|
package/src/statuses.mjs
CHANGED
|
@@ -451,7 +451,7 @@ async function runMigrateType(args, config, opts) {
|
|
|
451
451
|
if (lc.terminalStatuses.has(name)) props.terminal = true;
|
|
452
452
|
// Apply quiet sugar when both skipStale and skipWarnings hold; otherwise emit the individual flag.
|
|
453
453
|
const skipStale = lc.skipStaleFor.has(name);
|
|
454
|
-
const skipWarnings = lc.
|
|
454
|
+
const skipWarnings = lc.skipsWarnings(name, typeName);
|
|
455
455
|
if (skipStale && skipWarnings) props.quiet = true;
|
|
456
456
|
else {
|
|
457
457
|
if (skipStale) props.skipStale = true;
|
package/src/sync-status.mjs
CHANGED
|
@@ -89,16 +89,16 @@ export function collectHubStatusRows(docs, config, { hubPaths = null } = {}) {
|
|
|
89
89
|
const key = doc.path.toLowerCase();
|
|
90
90
|
docByFoldedPath.set(key, docByFoldedPath.has(key) ? null : doc);
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
// Per-doc: warning suppression is type-scoped, so a status name quiet for one
|
|
93
|
+
// type stays loud for another that declared the same name.
|
|
94
|
+
const quiet = (d) => config.lifecycle?.terminalStatuses?.has(d.status)
|
|
95
|
+
|| config.lifecycle?.skipsWarnings(d.status, d.type);
|
|
96
96
|
const out = [];
|
|
97
97
|
|
|
98
98
|
for (const hub of docs) {
|
|
99
99
|
if (!isHubDoc(hub)) continue;
|
|
100
100
|
if (hubPaths) { if (!hubPaths.has(hub.path)) continue; }
|
|
101
|
-
else if (quiet
|
|
101
|
+
else if (quiet(hub)) continue;
|
|
102
102
|
|
|
103
103
|
let raw;
|
|
104
104
|
try { raw = readFileSync(path.join(config.repoRoot, hub.path), 'utf8'); } catch { continue; }
|
package/src/validate.mjs
CHANGED
|
@@ -93,7 +93,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
93
93
|
|
|
94
94
|
const knownStatus = isValidStatus(doc.status, doc.root, config, doc.type);
|
|
95
95
|
|
|
96
|
-
if (knownStatus && !config.lifecycle.
|
|
96
|
+
if (knownStatus && !config.lifecycle.skipsWarnings(doc.status, doc.type) && !doc.updated) {
|
|
97
97
|
doc.errors.push({ path: doc.path, level: 'error', message: 'Missing frontmatter `updated` for non-archived doc.' });
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -130,7 +130,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
130
130
|
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.' });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if (config.validSurfaces && !config.lifecycle.
|
|
133
|
+
if (config.validSurfaces && !config.lifecycle.skipsWarnings(doc.status, doc.type)) {
|
|
134
134
|
const knownSurfaces = [...config.validSurfaces];
|
|
135
135
|
for (const surface of doc.surfaces) {
|
|
136
136
|
if (!config.validSurfaces.has(surface)) {
|
|
@@ -153,7 +153,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
153
153
|
// target is inlined in the message so `dotmd lint --fix` users see exactly
|
|
154
154
|
// what they'll end up with — and so non-fix readers can hand-migrate.
|
|
155
155
|
// Suppress for archived/terminal docs (same noise-control rule as F2).
|
|
156
|
-
if (!config.lifecycle.
|
|
156
|
+
if (!config.lifecycle.skipsWarnings(doc.status, doc.type)) {
|
|
157
157
|
for (const { singular, plural } of [{ singular: 'module', plural: 'modules' }, { singular: 'surface', plural: 'surfaces' }]) {
|
|
158
158
|
const singularValue = frontmatter[singular];
|
|
159
159
|
if (!Object.prototype.hasOwnProperty.call(frontmatter, singular)) continue;
|
|
@@ -190,13 +190,13 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
190
190
|
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Missing `title` and no H1 found for fallback.' });
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
if (!skipTitleSummary && !config.lifecycle.
|
|
193
|
+
if (!skipTitleSummary && !config.lifecycle.skipsWarnings(doc.status, doc.type) && !asString(frontmatter.summary) && !doc.summary) {
|
|
194
194
|
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Missing `summary` and no blockquote fallback found.' });
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// Determine which statuses should have current_state and next_step (plans only, not docs/research)
|
|
198
198
|
const isPlanWork = knownStatus && doc.status && (!doc.type || doc.type === 'plan')
|
|
199
|
-
&& !config.lifecycle.terminalStatuses.has(doc.status) && !config.lifecycle.
|
|
199
|
+
&& !config.lifecycle.terminalStatuses.has(doc.status) && !config.lifecycle.skipsWarnings(doc.status, doc.type);
|
|
200
200
|
|
|
201
201
|
if (isPlanWork && !asString(frontmatter.current_state)) {
|
|
202
202
|
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Missing `current_state`; index output is using a fallback or placeholder.' });
|
|
@@ -259,7 +259,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
259
259
|
const docDir = path.dirname(path.join(config.repoRoot, doc.path));
|
|
260
260
|
const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];
|
|
261
261
|
const skipRefValidation = config.lifecycle.terminalStatuses.has(doc.status)
|
|
262
|
-
|| config.lifecycle.
|
|
262
|
+
|| config.lifecycle.skipsWarnings(doc.status, doc.type);
|
|
263
263
|
if (!skipRefValidation) {
|
|
264
264
|
for (const field of allRefFields) {
|
|
265
265
|
for (const relPath of (doc.refFields[field] || [])) {
|
|
@@ -406,14 +406,14 @@ export function checkBidirectionalReferences(docs, config) {
|
|
|
406
406
|
// child it doesn't own without nagging the child to add `parent_plan:`.
|
|
407
407
|
export function checkRunlistBackPointers(docs, config) {
|
|
408
408
|
const warnings = [];
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
// Per-doc, not a precomputed name set: warning suppression is type-scoped, so
|
|
410
|
+
// the same status name can be quiet for one type and loud for another.
|
|
411
|
+
const skipDoc = (d) => config.lifecycle.terminalStatuses.has(d.status)
|
|
412
|
+
|| config.lifecycle.skipsWarnings(d.status, d.type);
|
|
413
413
|
const byPath = new Map(docs.map(d => [d.path, d]));
|
|
414
414
|
|
|
415
415
|
for (const hub of docs) {
|
|
416
|
-
if (
|
|
416
|
+
if (skipDoc(hub)) continue;
|
|
417
417
|
const runlistRefs = hub.refFields?.runlist ?? [];
|
|
418
418
|
const runlistDirs = hub.refFieldDirections?.runlist ?? [];
|
|
419
419
|
if (runlistRefs.length === 0) continue;
|
|
@@ -427,7 +427,7 @@ export function checkRunlistBackPointers(docs, config) {
|
|
|
427
427
|
const childPath = toRepoPath(resolved, config.repoRoot);
|
|
428
428
|
const child = byPath.get(childPath);
|
|
429
429
|
if (!child) continue;
|
|
430
|
-
if (
|
|
430
|
+
if (skipDoc(child)) continue;
|
|
431
431
|
const childParents = (child.refFields?.parent_plan ?? []).map(p => {
|
|
432
432
|
const abs = resolveRefPath(p, path.dirname(path.join(config.repoRoot, child.path)), config.repoRoot);
|
|
433
433
|
return abs ? toRepoPath(abs, config.repoRoot) : p;
|
|
@@ -451,13 +451,13 @@ export function checkRunlistBackPointers(docs, config) {
|
|
|
451
451
|
// it explicit. Skips terminal/quiet statuses like every other warning-only check.
|
|
452
452
|
export function checkCoordinationHubExecutionMode(docs, config) {
|
|
453
453
|
const warnings = [];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
454
|
+
// Per-doc, not a precomputed name set: warning suppression is type-scoped, so
|
|
455
|
+
// the same status name can be quiet for one type and loud for another.
|
|
456
|
+
const skipDoc = (d) => config.lifecycle.terminalStatuses.has(d.status)
|
|
457
|
+
|| config.lifecycle.skipsWarnings(d.status, d.type);
|
|
458
458
|
for (const doc of docs) {
|
|
459
459
|
if (doc.type && doc.type !== 'plan') continue;
|
|
460
|
-
if (
|
|
460
|
+
if (skipDoc(doc)) continue;
|
|
461
461
|
// A roadmap (`execution_mode: roadmap`) is already an explicit held-out hub —
|
|
462
462
|
// just a tier up. Don't nudge it toward `coordination` even when its slug is
|
|
463
463
|
// `*-runlist` (e.g. a `master-runlist` promoted to a roadmap).
|
|
@@ -482,10 +482,10 @@ export function checkCoordinationHubExecutionMode(docs, config) {
|
|
|
482
482
|
// coordination hub that merely references one sibling runlist isn't mislabelled.
|
|
483
483
|
export function checkRoadmapHubExecutionMode(docs, config) {
|
|
484
484
|
const warnings = [];
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
485
|
+
// Per-doc, not a precomputed name set: warning suppression is type-scoped, so
|
|
486
|
+
// the same status name can be quiet for one type and loud for another.
|
|
487
|
+
const skipDoc = (d) => config.lifecycle.terminalStatuses.has(d.status)
|
|
488
|
+
|| config.lifecycle.skipsWarnings(d.status, d.type);
|
|
489
489
|
const byPath = new Map(docs.map(d => [d.path, d]));
|
|
490
490
|
const byBasename = new Map();
|
|
491
491
|
for (const d of docs) {
|
|
@@ -501,7 +501,7 @@ export function checkRoadmapHubExecutionMode(docs, config) {
|
|
|
501
501
|
};
|
|
502
502
|
for (const doc of docs) {
|
|
503
503
|
if (doc.type && doc.type !== 'plan') continue;
|
|
504
|
-
if (
|
|
504
|
+
if (skipDoc(doc)) continue;
|
|
505
505
|
if (doc.executionMode === 'roadmap') continue; // already a roadmap
|
|
506
506
|
if (doc.executionMode !== 'coordination') continue; // only nudge explicit coordination hubs
|
|
507
507
|
const refs = doc.refFields?.related_plans ?? [];
|
|
@@ -585,7 +585,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
|
|
|
585
585
|
if (doc.type !== 'plan') return;
|
|
586
586
|
// Skip plans in terminal/archive statuses (closed work shouldn't generate noise)
|
|
587
587
|
if (config.lifecycle.terminalStatuses.has(doc.status) || config.lifecycle.archiveStatuses.has(doc.status)) return;
|
|
588
|
-
if (config.lifecycle.
|
|
588
|
+
if (config.lifecycle.skipsWarnings(doc.status, doc.type)) return;
|
|
589
589
|
|
|
590
590
|
// 1. next_step length cap (800 chars). Was 300; raised in parallel with
|
|
591
591
|
// current_state for the same reason: agents need to encode "what to do next"
|
|
@@ -658,7 +658,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
|
|
|
658
658
|
export function validateDocShape(doc, body, frontmatter, config) {
|
|
659
659
|
if (doc.type !== 'doc') return;
|
|
660
660
|
if (config.lifecycle.terminalStatuses.has(doc.status) || config.lifecycle.archiveStatuses.has(doc.status)) return;
|
|
661
|
-
if (config.lifecycle.
|
|
661
|
+
if (config.lifecycle.skipsWarnings(doc.status, doc.type)) return;
|
|
662
662
|
|
|
663
663
|
if (!body) return;
|
|
664
664
|
|