dotmd-cli 0.62.0 → 0.63.0
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 -3
- package/package.json +1 -1
- package/src/health.mjs +5 -4
- package/src/lifecycle.mjs +45 -13
- package/src/query.mjs +12 -2
- package/src/runlist.mjs +88 -11
package/bin/dotmd.mjs
CHANGED
|
@@ -1225,14 +1225,19 @@ when the hub is \`planned\`) still render on their own.
|
|
|
1225
1225
|
Larger, prose-first "coordination" runlists (a domain map pointing at many
|
|
1226
1226
|
plans, marked \`execution_mode: coordination\` or named \`*-runlist\`) aren't
|
|
1227
1227
|
folded — they're lifted into a separate \`Runlists\` section in \`dotmd plans\`
|
|
1228
|
-
and out of the active count. \`dotmd runlists\` shows that dashboard on its own
|
|
1228
|
+
and out of the active count. \`dotmd runlists\` shows that dashboard on its own.
|
|
1229
|
+
For these, \`runlist\`/\`runlist next\` also read order from the body when there's
|
|
1230
|
+
no \`runlist:\` array — a \`## Ranked queue\` table or \`## Order of operations\`
|
|
1231
|
+
list of markdown links (the first \`.md\` link per row/item, in order).`,
|
|
1229
1232
|
|
|
1230
1233
|
runlists: `dotmd runlists — the coordination-hub dashboard
|
|
1231
1234
|
|
|
1232
1235
|
Lists every *coordination runlist*: a prose-first plan that sits above a
|
|
1233
1236
|
cluster of others (a domain map), detected by \`execution_mode: coordination\`
|
|
1234
1237
|
or a \`*-runlist\` / \`runlist\` slug. Each row shows the hub, its age, the rough
|
|
1235
|
-
size of its \`related_plans:\` cluster,
|
|
1238
|
+
size of its \`related_plans:\` cluster, a \`next → <child>\` when the hub's body
|
|
1239
|
+
encodes order as markdown links (\`## Ranked queue\` table / \`## Order of
|
|
1240
|
+
operations\` list), and a one-line descriptor.
|
|
1236
1241
|
|
|
1237
1242
|
This is the standalone form of the \`Runlists\` section that \`dotmd plans\`
|
|
1238
1243
|
pins beneath the leaf-plan triage list.
|
|
@@ -1240,7 +1245,7 @@ pins beneath the leaf-plan triage list.
|
|
|
1240
1245
|
dotmd runlists All runlists (a small bounded set), most stale first.
|
|
1241
1246
|
dotmd runlists --sort recent Order by recency instead (age|recent|related|title|status).
|
|
1242
1247
|
dotmd runlists --limit N Cap the list at N.
|
|
1243
|
-
dotmd runlists --json Structured rows (path, status, childCount, …).`,
|
|
1248
|
+
dotmd runlists --json Structured rows (path, status, childCount, nextPickup, …).`,
|
|
1244
1249
|
|
|
1245
1250
|
'bulk-tag': `dotmd bulk-tag [files...] — fill in type/status frontmatter on pre-existing markdown
|
|
1246
1251
|
|
package/package.json
CHANGED
package/src/health.mjs
CHANGED
|
@@ -87,7 +87,7 @@ export function runHealth(argv, config) {
|
|
|
87
87
|
ready: { count: readyPlans.length },
|
|
88
88
|
planned: { count: plannedPlans.length },
|
|
89
89
|
recentlyArchived: { count: recentlyArchived.length, last30d: recentlyArchived.map(d => path.basename(d.path, '.md')) },
|
|
90
|
-
runlists: { count: runlistHubs.length, hubs: runlistHubs.map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0 })) },
|
|
90
|
+
runlists: { count: runlistHubs.length, hubs: runlistHubs.map(d => ({ path: d.path, title: d.title, status: d.status, childCount: coordination.get(d.path)?.childCount ?? 0, nextPickup: coordination.get(d.path)?.nextPickup ?? null })) },
|
|
91
91
|
}, null, 2) + '\n');
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
@@ -122,9 +122,10 @@ export function runHealth(argv, config) {
|
|
|
122
122
|
for (const doc of runlistHubs.slice(0, 8)) {
|
|
123
123
|
const slug = hubLabel(doc).padEnd(28);
|
|
124
124
|
const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : '?d';
|
|
125
|
-
const
|
|
126
|
-
const relStr =
|
|
127
|
-
|
|
125
|
+
const info = coordination.get(doc.path);
|
|
126
|
+
const relStr = info?.childCount ? ` ${dim(`${info.childCount} related`)}` : '';
|
|
127
|
+
const nextStr = info?.nextPickup ? ` ${green('→')} ${info.nextPickup.label}` : '';
|
|
128
|
+
process.stdout.write(` ${slug} ${dim(age.padStart(4))}${relStr}${nextStr}\n`);
|
|
128
129
|
}
|
|
129
130
|
if (runlistHubs.length > 8) {
|
|
130
131
|
process.stdout.write(` ${dim(`...and ${runlistHubs.length - 8} more`)}\n`);
|
package/src/lifecycle.mjs
CHANGED
|
@@ -236,6 +236,10 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
236
236
|
process.stdout.write(`${prefix} Would unfile: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
237
237
|
finalPath = targetPath;
|
|
238
238
|
}
|
|
239
|
+
if (finalPath !== filePath) {
|
|
240
|
+
const refCount = countRefsToUpdate(filePath, finalPath, config);
|
|
241
|
+
if (refCount > 0) process.stdout.write(`${prefix} Would update references in ${refCount} file(s)\n`);
|
|
242
|
+
}
|
|
239
243
|
if ((isArchiving || isUnarchiving || isFiling || isUnfiling) && config.indexPath) {
|
|
240
244
|
process.stdout.write(`${prefix} Would regenerate index\n`);
|
|
241
245
|
}
|
|
@@ -286,6 +290,22 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
286
290
|
finalPath = targetPath;
|
|
287
291
|
}
|
|
288
292
|
|
|
293
|
+
// Any of the four moves above shifts the file's directory, which breaks
|
|
294
|
+
// relative refs in both directions — links FROM the moved file and inbound
|
|
295
|
+
// refs TO it from other docs. runArchive repairs both; mirror that here so
|
|
296
|
+
// the deprecated `dotmd status <file> archived` path and the `dotmd set`
|
|
297
|
+
// unarchive/file/unfile transitions (which route through runStatus, not
|
|
298
|
+
// runArchive) don't silently leave dangling links.
|
|
299
|
+
let selfRefsFixed = false;
|
|
300
|
+
let inboundRefCount = 0;
|
|
301
|
+
let inboundRefPaths = [];
|
|
302
|
+
if (finalPath !== filePath) {
|
|
303
|
+
selfRefsFixed = updateRefsFromMovedFile(filePath, finalPath, config) > 0;
|
|
304
|
+
const inbound = updateRefsAfterMove(filePath, finalPath, config);
|
|
305
|
+
inboundRefCount = inbound.count;
|
|
306
|
+
inboundRefPaths = inbound.paths;
|
|
307
|
+
}
|
|
308
|
+
|
|
289
309
|
// Regen the index on every status change — `active → planned` etc. drift
|
|
290
310
|
// the per-status sections just as much as archive crossings. Archive paths
|
|
291
311
|
// also benefit (replaces the previously-gated regen). `--no-index` skips
|
|
@@ -298,10 +318,13 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
298
318
|
}
|
|
299
319
|
|
|
300
320
|
process.stdout.write(`${green(toRepoPath(finalPath, config.repoRoot))}: ${oldStatus ?? 'unknown'} → ${newStatus}\n`);
|
|
321
|
+
if (selfRefsFixed) process.stdout.write('Updated references in moved file.\n');
|
|
322
|
+
if (inboundRefCount > 0) process.stdout.write(`Updated references in ${inboundRefCount} file(s).\n`);
|
|
301
323
|
|
|
302
324
|
if (showFiles) {
|
|
303
325
|
const touched = [filePath];
|
|
304
326
|
if (finalPath !== filePath) touched.push(finalPath);
|
|
327
|
+
touched.push(...inboundRefPaths);
|
|
305
328
|
if (config.indexPath && !noIndex) touched.push(config.indexPath);
|
|
306
329
|
emitFilesFooter(touched, config);
|
|
307
330
|
}
|
|
@@ -749,6 +772,26 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
749
772
|
try { config.hooks.onTouch?.({ path: toRepoPath(filePath, config.repoRoot) }, { path: toRepoPath(filePath, config.repoRoot), date: today }); } catch (err) { warn(`Hook 'onTouch' threw: ${err.message}`); }
|
|
750
773
|
}
|
|
751
774
|
|
|
775
|
+
// Rewrite every frontmatter ref token (a `*.md` path in a YAML list item or an
|
|
776
|
+
// inline scalar, quoted or `>`-prefixed) that points at `oldPath` so it points
|
|
777
|
+
// at `newPath`. Each token is resolved doc-relative *and* repo-relative and
|
|
778
|
+
// compared to oldPath by absolute path — mirroring how the body-link branch and
|
|
779
|
+
// `updateRefsFromMovedFile` resolve refs. This replaces an older substring
|
|
780
|
+
// rewrite (`fm.split(oldRelPath).join(newRelPath)`) that only knew doc-relative
|
|
781
|
+
// paths, so it: left repo-relative cross-dir refs (`docs/plans/child.md` from
|
|
782
|
+
// `docs/rfcs/spec.md`) broken; mangled same-dir repo-relative refs into
|
|
783
|
+
// `docs/plans/../archived/child.md`; and could corrupt a `grandchild.md` ref
|
|
784
|
+
// when archiving `child.md` (suffix match). oldPath no longer exists on disk
|
|
785
|
+
// post-`git mv`, so existsSync-based resolveRefPath can't be used here.
|
|
786
|
+
function rewriteFrontmatterRefs(fm, docDir, oldPath, newPath, repoRoot) {
|
|
787
|
+
return fm.replace(/[^\s"'<>:]+\.md\b/g, (token) => {
|
|
788
|
+
const docRelAbs = path.resolve(docDir, token);
|
|
789
|
+
const repoRelAbs = path.resolve(repoRoot, token);
|
|
790
|
+
if (docRelAbs !== oldPath && repoRelAbs !== oldPath) return token;
|
|
791
|
+
return path.relative(docDir, newPath).split(path.sep).join('/');
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
752
795
|
/**
|
|
753
796
|
* After a file moves (archive/unarchive), update frontmatter references in all
|
|
754
797
|
* docs that pointed to the old location so they point to the new one.
|
|
@@ -766,17 +809,7 @@ function updateRefsAfterMove(oldPath, newPath, config) {
|
|
|
766
809
|
if (!fm) continue;
|
|
767
810
|
|
|
768
811
|
const docDir = path.dirname(docFile);
|
|
769
|
-
const
|
|
770
|
-
const newRelPath = path.relative(docDir, newPath).split(path.sep).join('/');
|
|
771
|
-
|
|
772
|
-
let newFm = fm;
|
|
773
|
-
if (newFm.includes(oldRelPath)) {
|
|
774
|
-
newFm = newFm.split(oldRelPath).join(newRelPath);
|
|
775
|
-
}
|
|
776
|
-
const dotSlashOld = './' + oldRelPath;
|
|
777
|
-
if (newFm.includes(dotSlashOld)) {
|
|
778
|
-
newFm = newFm.split(dotSlashOld).join(newRelPath);
|
|
779
|
-
}
|
|
812
|
+
const newFm = rewriteFrontmatterRefs(fm, docDir, oldPath, newPath, config.repoRoot);
|
|
780
813
|
|
|
781
814
|
// Body markdown links [text](path.md) or [text](path.md#anchor) pointing
|
|
782
815
|
// at oldPath. resolveRefPath can't be used here: oldPath no longer exists
|
|
@@ -856,8 +889,7 @@ function countRefsToUpdate(oldPath, newPath, config) {
|
|
|
856
889
|
if (!fm) continue;
|
|
857
890
|
|
|
858
891
|
const docDir = path.dirname(docFile);
|
|
859
|
-
const
|
|
860
|
-
const fmHit = fm.includes(oldRelPath) || fm.includes('./' + oldRelPath);
|
|
892
|
+
const fmHit = rewriteFrontmatterRefs(fm, docDir, oldPath, newPath, config.repoRoot) !== fm;
|
|
861
893
|
|
|
862
894
|
let bodyHit = false;
|
|
863
895
|
if (!fmHit) {
|
package/src/query.mjs
CHANGED
|
@@ -162,6 +162,7 @@ export function runRunlists(index, argv, config) {
|
|
|
162
162
|
status: d.status,
|
|
163
163
|
title: d.title,
|
|
164
164
|
childCount: coordination.get(d.path)?.childCount ?? 0,
|
|
165
|
+
nextPickup: coordination.get(d.path)?.nextPickup ?? null,
|
|
165
166
|
updated: d.updated,
|
|
166
167
|
nextStep: d.nextStep ?? null,
|
|
167
168
|
}));
|
|
@@ -782,9 +783,18 @@ function renderCoordinationSection(coordDocs, coordination, maxWidth, total) {
|
|
|
782
783
|
const statusTag = doc.status && doc.status !== 'active' ? ` ${colorTag(doc.status)}` : '';
|
|
783
784
|
const desc = (doc.nextStep || doc.currentState || doc.title || '').replace(/\s+/g, ' ').trim();
|
|
784
785
|
|
|
786
|
+
// Next-pickup (first non-archived ranked child from the body) leads the
|
|
787
|
+
// description column when one resolves — it's the most actionable cell. Its
|
|
788
|
+
// status is shown only when it's not the expected `active`.
|
|
789
|
+
const next = info?.nextPickup;
|
|
790
|
+
const nextStr = next
|
|
791
|
+
? `${green('→')} ${next.label}${next.status && next.status !== 'active' ? dim(` (${next.status})`) : ''}`
|
|
792
|
+
: '';
|
|
793
|
+
const nextPart = nextStr ? `${nextStr} ` : '';
|
|
794
|
+
|
|
785
795
|
const left = ` ${slug} ${ageStr} ${dim(count)} `;
|
|
786
|
-
const budget = Math.max(10, maxWidth - visibleLen(left) - visibleLen(statusTag) - 2);
|
|
796
|
+
const budget = Math.max(10, maxWidth - visibleLen(left) - visibleLen(nextPart) - visibleLen(statusTag) - 2);
|
|
787
797
|
const descR = desc.length > budget ? desc.slice(0, budget - 3) + '...' : desc;
|
|
788
|
-
process.stdout.write(`${left}${dim(descR)}${statusTag}\n`);
|
|
798
|
+
process.stdout.write(`${left}${nextPart}${dim(descR)}${statusTag}\n`);
|
|
789
799
|
}
|
|
790
800
|
}
|
package/src/runlist.mjs
CHANGED
|
@@ -89,6 +89,10 @@ export function isCoordinationHub(doc) {
|
|
|
89
89
|
// cluster (resolved against the index; peers/self excluded). It's an
|
|
90
90
|
// approximation — `related_plans` is a *related* cluster, not a strict child
|
|
91
91
|
// list — so it's shown as a rough "N plans" hint, not an authoritative count.
|
|
92
|
+
// Each hub also gets a `nextPickup` (or null) parsed from its body order — the
|
|
93
|
+
// first non-archived ranked child — so prose-first hubs surface a next-pickup
|
|
94
|
+
// target the way sprint `runlist:` hubs do. Reads each hub's file (a small,
|
|
95
|
+
// bounded set), so this is no longer pure in-memory like `buildRunlistIndex`.
|
|
92
96
|
export function buildCoordinationIndex(index, config) {
|
|
93
97
|
const docByPath = new Map(index.docs.map(d => [d.path, d]));
|
|
94
98
|
const byBasename = new Map();
|
|
@@ -96,6 +100,15 @@ export function buildCoordinationIndex(index, config) {
|
|
|
96
100
|
const base = d.path.split('/').pop();
|
|
97
101
|
if (!byBasename.has(base)) byBasename.set(base, d);
|
|
98
102
|
}
|
|
103
|
+
const archiveStatuses = config.lifecycle?.archiveStatuses ?? new Set(['archived']);
|
|
104
|
+
|
|
105
|
+
// Resolve a path-or-basename ref (from frontmatter or body) to an indexed doc.
|
|
106
|
+
const resolveRef = (ref, dir) => {
|
|
107
|
+
const abs = resolveRefPath(ref, dir, config.repoRoot);
|
|
108
|
+
let child = abs ? docByPath.get(toRepoPath(abs, config.repoRoot)) ?? null : null;
|
|
109
|
+
if (!child) child = byBasename.get(ref.split('/').pop()) ?? null;
|
|
110
|
+
return child;
|
|
111
|
+
};
|
|
99
112
|
|
|
100
113
|
const hubs = new Map();
|
|
101
114
|
for (const doc of index.docs) {
|
|
@@ -104,14 +117,13 @@ export function buildCoordinationIndex(index, config) {
|
|
|
104
117
|
const refs = doc.refFields?.related_plans ?? [];
|
|
105
118
|
const childPaths = new Set();
|
|
106
119
|
for (const ref of refs) {
|
|
107
|
-
const
|
|
108
|
-
let child = abs ? docByPath.get(toRepoPath(abs, config.repoRoot)) ?? null : null;
|
|
109
|
-
if (!child) child = byBasename.get(ref.split('/').pop()) ?? null;
|
|
120
|
+
const child = resolveRef(ref, dir);
|
|
110
121
|
if (child && child.path !== doc.path && (child.type === 'plan' || child.type == null)) {
|
|
111
122
|
childPaths.add(child.path);
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
|
-
|
|
125
|
+
const nextPickup = resolveHubNextPickup(doc, dir, resolveRef, archiveStatuses, config);
|
|
126
|
+
hubs.set(doc.path, { doc, childCount: childPaths.size, childPaths, nextPickup });
|
|
115
127
|
}
|
|
116
128
|
return hubs;
|
|
117
129
|
}
|
|
@@ -169,28 +181,93 @@ function resolveRunlistRefs(refs, hubAbsPath, config) {
|
|
|
169
181
|
return out;
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
// Extract ordered plan refs from a hub's body prose. Two shapes:
|
|
185
|
+
// - link-list sections (`## Order of operations`, `## Runlist`, …) — every
|
|
186
|
+
// `.md` link or checklist item, in document order.
|
|
187
|
+
// - ranked-queue tables (`## Ranked queue`, …) — the first `.md` link in each
|
|
188
|
+
// table row (the ranked plan); header/separator rows contribute none.
|
|
189
|
+
// Coordination hubs encode their next-pickup order in the table shape; sprint-
|
|
190
|
+
// ish hubs use the link list. Deduped, first occurrence wins, order preserved.
|
|
172
191
|
function detectBodyRunlistRefs(body) {
|
|
173
192
|
if (!body) return [];
|
|
174
|
-
const sectionRe = /^##\s+(Order of operations|Runlist|Execution order|Implementation order|Plan order)\s*$/gim;
|
|
175
193
|
const refs = [];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const start = match.index + match[0].length;
|
|
194
|
+
const linkRe = /\[[^\]]+\]\(([^)]+\.md(?:#[^)]+)?)\)/;
|
|
195
|
+
const sliceSection = (start) => {
|
|
179
196
|
const rest = body.slice(start);
|
|
180
197
|
const next = rest.search(/^##\s+/m);
|
|
181
|
-
|
|
198
|
+
return next >= 0 ? rest.slice(0, next) : rest;
|
|
199
|
+
};
|
|
182
200
|
|
|
183
|
-
|
|
201
|
+
const linkSectionRe = /^##\s+(?:Order of operations|Runlist|Execution order|Implementation order|Plan order)\b.*$/gim;
|
|
202
|
+
let match;
|
|
203
|
+
while ((match = linkSectionRe.exec(body)) !== null) {
|
|
204
|
+
const section = sliceSection(match.index + match[0].length);
|
|
205
|
+
const allLinks = new RegExp(linkRe.source, 'g');
|
|
184
206
|
let link;
|
|
185
|
-
while ((link =
|
|
207
|
+
while ((link = allLinks.exec(section)) !== null) refs.push(link[1]);
|
|
186
208
|
|
|
187
209
|
const checklistRe = /^\s*[-*]\s+\[[ xX]\]\s+([^\s)]+\.md(?:#[^\s)]+)?)/gm;
|
|
188
210
|
let item;
|
|
189
211
|
while ((item = checklistRe.exec(section)) !== null) refs.push(item[1]);
|
|
190
212
|
}
|
|
213
|
+
|
|
214
|
+
// Ranked-queue tables: the first `.md` link per row is the ranked plan. A
|
|
215
|
+
// header (`| Rank | Plan | … |`) and separator (`|---|`) carry no link and are
|
|
216
|
+
// skipped naturally. Heading may carry trailing text (`## Ranked queue (next
|
|
217
|
+
// pickup)`), so match the leading words, not an exact line.
|
|
218
|
+
const queueSectionRe = /^##\s+(?:Ranked queue|Queue|Pickup order|Heads)\b.*$/gim;
|
|
219
|
+
while ((match = queueSectionRe.exec(body)) !== null) {
|
|
220
|
+
const section = sliceSection(match.index + match[0].length);
|
|
221
|
+
for (const rawLine of section.split('\n')) {
|
|
222
|
+
const line = rawLine.trim();
|
|
223
|
+
if (!line.startsWith('|')) continue;
|
|
224
|
+
const link = linkRe.exec(line);
|
|
225
|
+
if (link) refs.push(link[1]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
191
229
|
return [...new Set(refs)];
|
|
192
230
|
}
|
|
193
231
|
|
|
232
|
+
// Label for a hub's next-pickup child: its slug with the hub's leading module
|
|
233
|
+
// segment stripped when shared (so `founder-runlist` → `founder-brand-conflicts`
|
|
234
|
+
// reads as `brand-conflicts`), mirroring how sprint children drop the hub
|
|
235
|
+
// prefix. Falls back to the full slug when there's no shared leading segment.
|
|
236
|
+
function coordinationChildLabel(childDoc, hubDoc) {
|
|
237
|
+
const childSlug = toSlug(childDoc);
|
|
238
|
+
const seg = toSlug(hubDoc).split('-')[0];
|
|
239
|
+
if (seg.length >= 2 && childSlug.startsWith(`${seg}-`) && childSlug.length > seg.length + 1) {
|
|
240
|
+
return childSlug.slice(seg.length + 1);
|
|
241
|
+
}
|
|
242
|
+
return childSlug;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Read a coordination hub's body order (a `## Ranked queue` table or a
|
|
246
|
+
// `## Order of operations` link list) and return its NEXT PICKUP: the first
|
|
247
|
+
// ranked child that isn't archived, resolved to its live status from the index.
|
|
248
|
+
// Prose-first hubs keep their sequence in the body, invisible to the
|
|
249
|
+
// frontmatter-only index — this surfaces `next → <child>` the way sprint
|
|
250
|
+
// `runlist:` hubs already do. Returns null when the hub has no parseable body
|
|
251
|
+
// order or every ranked child is archived. Best-effort: a read failure degrades
|
|
252
|
+
// to null, never throws.
|
|
253
|
+
function resolveHubNextPickup(hubDoc, hubDir, resolveRef, archiveStatuses, config) {
|
|
254
|
+
let body;
|
|
255
|
+
try {
|
|
256
|
+
({ body } = extractFrontmatter(readFileSync(path.join(config.repoRoot, hubDoc.path), 'utf8')));
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
for (const ref of detectBodyRunlistRefs(body)) {
|
|
261
|
+
const child = resolveRef(ref, hubDir);
|
|
262
|
+
if (!child || child.path === hubDoc.path) continue;
|
|
263
|
+
if (child.type && child.type !== 'plan') continue;
|
|
264
|
+
const archived = archiveStatuses.has(child.status) || isArchivedPath(child.path, config);
|
|
265
|
+
if (archived) continue;
|
|
266
|
+
return { path: child.path, status: child.status ?? null, label: coordinationChildLabel(child, hubDoc) };
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
194
271
|
function readRunlistChildren(hubAbsPath, config) {
|
|
195
272
|
const raw = readFileSync(hubAbsPath, 'utf8');
|
|
196
273
|
const { frontmatter: fmRaw, body } = extractFrontmatter(raw);
|