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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.74.4",
3
+ "version": "0.74.5",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 and inline code to avoid false positives
45
- const stripped = body.replace(/^```[\s\S]*?^```/gm, '').replace(/`[^`]+`/g, '');
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
- let link;
247
- while ((link = allLinks.exec(section)) !== null) refs.push(link[1]);
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
- // A roadmap (`execution_mode: roadmap`) is already an explicit held-out hub —
462
- // just a tier up. Don't nudge it toward `coordination` even when its slug is
463
- // `*-runlist` (e.g. a `master-runlist` promoted to a roadmap).
464
- if (doc.executionMode === 'coordination' || doc.executionMode === 'roadmap') continue;
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({