dotmd-cli 0.74.4 → 0.74.5
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/extractors.mjs +11 -2
- package/src/hub.mjs +24 -2
- package/src/validate.mjs +8 -4
package/package.json
CHANGED
package/src/extractors.mjs
CHANGED
|
@@ -41,8 +41,17 @@ export function extractNextStep(body) {
|
|
|
41
41
|
|
|
42
42
|
export function extractBodyLinks(body) {
|
|
43
43
|
if (!body) return [];
|
|
44
|
-
// Strip fenced code blocks
|
|
45
|
-
|
|
44
|
+
// Strip fenced code blocks, then MASK inline code rather than delete it.
|
|
45
|
+
// Deleting it ate the commonest link idiom in a plan hub: [`plan.md`](plan.md)
|
|
46
|
+
// has its link TEXT as a code span, so removing the span left `[](plan.md)`,
|
|
47
|
+
// which the regex below rejects for having empty text. A hub with hundreds of
|
|
48
|
+
// such links reported one — and since this list is what validates body links,
|
|
49
|
+
// every one of them was also never checked for breakage. Masking to same-length
|
|
50
|
+
// filler keeps the link matchable while still neutralizing a link that is
|
|
51
|
+
// itself inside code (`[fake](x.md)` stays unmatched), and preserves offsets.
|
|
52
|
+
const stripped = body
|
|
53
|
+
.replace(/^```[\s\S]*?^```/gm, '')
|
|
54
|
+
.replace(/`[^`]+`/g, match => 'x'.repeat(match.length));
|
|
46
55
|
const links = [];
|
|
47
56
|
// Match [text](path.md) or [text](path.md#anchor), skip images (preceded by !)
|
|
48
57
|
const regex = /(?<!!)\[([^\]]+)\]\(([^)]+\.md(?:#[^)]*)?)\)/g;
|
package/src/hub.mjs
CHANGED
|
@@ -242,9 +242,31 @@ export function detectBodyRunlistRefs(body) {
|
|
|
242
242
|
let match;
|
|
243
243
|
while ((match = linkSectionRe.exec(body)) !== null) {
|
|
244
244
|
const section = sliceSection(match.index + match[0].length);
|
|
245
|
+
// Line by line, because the SHAPE decides what a link means, not the
|
|
246
|
+
// heading's name. A section titled `## Runlist index — by category` matches
|
|
247
|
+
// this heading list but is written as tables, and taking every link in it
|
|
248
|
+
// made each row's descriptive prose a membership claim: a row about plan A
|
|
249
|
+
// whose prose says "spawned children B, C" ranked B and C as the hub's own,
|
|
250
|
+
// then warned B and C for not naming the hub as parent. In a table, only the
|
|
251
|
+
// row's first link is the ranked plan — the same rule the ranked-queue
|
|
252
|
+
// branch below already applies.
|
|
245
253
|
const allLinks = new RegExp(linkRe.source, 'g');
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
for (const rawLine of section.split('\n')) {
|
|
255
|
+
const line = rawLine.trim();
|
|
256
|
+
if (line.startsWith('|')) {
|
|
257
|
+
const link = firstRowLink(line);
|
|
258
|
+
if (link) refs.push(link);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// One ranked item per line, first link wins — the same rule as a table
|
|
262
|
+
// row. A ranked item routinely carries commentary that links elsewhere
|
|
263
|
+
// ("Phase 7's open item closes on a route decision in [other-plan.md]"),
|
|
264
|
+
// and counting those made the hub claim plans it was only citing. A hub
|
|
265
|
+
// that means to rank two plans lists them on two lines.
|
|
266
|
+
allLinks.lastIndex = 0;
|
|
267
|
+
const link = allLinks.exec(line);
|
|
268
|
+
if (link) refs.push(link[1]);
|
|
269
|
+
}
|
|
248
270
|
|
|
249
271
|
const checklistRe = /^\s*[-*]\s+\[[ xX]\]\s+([^\s)]+\.md(?:#[^\s)]+)?)/gm;
|
|
250
272
|
let item;
|
package/src/validate.mjs
CHANGED
|
@@ -458,10 +458,14 @@ export function checkCoordinationHubExecutionMode(docs, config) {
|
|
|
458
458
|
for (const doc of docs) {
|
|
459
459
|
if (doc.type && doc.type !== 'plan') continue;
|
|
460
460
|
if (skipDoc(doc)) continue;
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
|
|
461
|
+
// ANY explicit `execution_mode` ends it. The field is the canonical signal
|
|
462
|
+
// and the slug is only a fallback for hubs that predate it, so a plan that
|
|
463
|
+
// already answered — `implementation`, `roadmap`, anything — has said what it
|
|
464
|
+
// is. Nudging past that misfires on exactly one population: plans ABOUT
|
|
465
|
+
// runlists, whose slug contains the word while the document is ordinary work.
|
|
466
|
+
// (A real one: a plan to rename the runlist concept, declared
|
|
467
|
+
// `execution_mode: implementation`, nagged to call itself a coordination hub.)
|
|
468
|
+
if (doc.executionMode) continue;
|
|
465
469
|
const base = (doc.path.split('/').pop() || '').replace(/\.md$/, '');
|
|
466
470
|
if (base !== 'runlist' && !base.endsWith('-runlist')) continue;
|
|
467
471
|
warnings.push({
|