runlist 0.0.0 → 0.94.2

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1112 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +296 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/public-inspection.mjs +19 -0
  136. package/src/query.mjs +977 -0
  137. package/src/record-action.mjs +11 -0
  138. package/src/record-create.mjs +82 -0
  139. package/src/reference-planner.mjs +388 -0
  140. package/src/reference-values.mjs +11 -0
  141. package/src/rename.mjs +107 -0
  142. package/src/render.mjs +678 -0
  143. package/src/roadmap.mjs +184 -0
  144. package/src/runlist.mjs +944 -0
  145. package/src/section.mjs +225 -0
  146. package/src/semantic-search-client.mjs +58 -0
  147. package/src/ship.mjs +166 -0
  148. package/src/show.mjs +101 -0
  149. package/src/skill-drift.mjs +98 -0
  150. package/src/source-editor.mjs +491 -0
  151. package/src/stages.mjs +73 -0
  152. package/src/state-migration.mjs +123 -0
  153. package/src/stats.mjs +200 -0
  154. package/src/status-metadata.mjs +87 -0
  155. package/src/statuses.mjs +721 -0
  156. package/src/summary.mjs +74 -0
  157. package/src/surfaces.mjs +28 -0
  158. package/src/sync-status.mjs +316 -0
  159. package/src/template-store.mjs +62 -0
  160. package/src/update.mjs +338 -0
  161. package/src/use.mjs +58 -0
  162. package/src/util.mjs +337 -0
  163. package/src/validate.mjs +795 -0
  164. package/src/watch.mjs +56 -0
  165. package/src/xref.mjs +406 -0
  166. package/src/yardstick-command.mjs +178 -0
  167. package/src/yardstick.mjs +112 -0
package/src/hub.mjs ADDED
@@ -0,0 +1,296 @@
1
+ // Hub primitives: what counts as a hub, and how to read a hub's body tables.
2
+ //
3
+ // This module is deliberately a LEAF — it imports nothing from runlist. The hub
4
+ // predicates used to live in `runlist.mjs`, but `runlist.mjs` imports
5
+ // `index.mjs` (for `resolveDocArg`), and the hub-status check has to run FROM
6
+ // `index.mjs`. Keeping the predicates here is what lets both sides share one
7
+ // definition of "hub" instead of re-deriving it — the exact duplication the
8
+ // hub-status-drift plan exists to avoid. `runlist.mjs` re-exports them so every
9
+ // existing importer is unaffected.
10
+
11
+ // A *coordination hub* is a prose-first plan that sits above a cluster of other
12
+ // plans — a "runlist" in the platform sense (master-runlist, ai-runlist, …)
13
+ // rather than a strictly-ordered frontmatter `runlist:` sprint. The signal is
14
+ // already in frontmatter (`execution_mode: coordination`), with the
15
+ // `*-runlist` / `runlist` naming convention as a fallback for the few hubs that
16
+ // predate the field. These plans aren't units of executable work — they're
17
+ // navigation maps — so the triage view tags them and lifts them out of the
18
+ // leaf-plan flow rather than treating them as one more active plan.
19
+ export function isCoordinationHub(doc) {
20
+ if (!doc) return false;
21
+ if (doc.type && doc.type !== 'plan') return false;
22
+ // Broad "held-out navigational hub" predicate: both coordination hubs and the
23
+ // tier-3 roadmap (`execution_mode: roadmap`) are lifted out of the active count
24
+ // and into a hub section. `isRoadmapHub` is the finer split the tier-3 views
25
+ // use to promote a roadmap above the Runlists section; here a roadmap counts as
26
+ // a coordination hub so all the existing held-out plumbing covers it for free.
27
+ if (doc.executionMode === 'coordination' || doc.executionMode === 'roadmap') return true;
28
+ // Any other declared `execution_mode` has answered: the name is only a fallback
29
+ // for hubs that predate the field, so a plan ABOUT runlists that declares
30
+ // `implementation` stays a plan. Same rule as the slug nudge in validate.mjs.
31
+ if (doc.executionMode) return false;
32
+ const base = (doc.path.split('/').pop() || '').replace(/\.md$/, '');
33
+ return base === 'runlist' || base.endsWith('-runlist');
34
+ }
35
+
36
+ // A *roadmap* is the tier-3 hub: a coordination hub whose children are themselves
37
+ // hubs (runlists / coordination hubs), with progress rolled up across them. The
38
+ // signal is explicit — `execution_mode: roadmap` — with NO slug-convention
39
+ // fallback (unlike coordination hubs' `*-runlist`): there's no naming convention
40
+ // for roadmaps, and `runlist check` nudges the structural case (a coordination hub
41
+ // that points at other hubs) toward the explicit field rather than auto-promoting.
42
+ export function isRoadmapHub(doc) {
43
+ if (!doc) return false;
44
+ if (doc.type && doc.type !== 'plan') return false;
45
+ return doc.executionMode === 'roadmap';
46
+ }
47
+
48
+ // A *sprint hub* declares its ordered children in frontmatter (`runlist:`).
49
+ export function isSprintHub(doc) {
50
+ return (doc?.refFields?.runlist ?? []).length > 0;
51
+ }
52
+
53
+ // Every shape of hub: a frontmatter sprint, a coordination map, or a roadmap.
54
+ // This is the set whose body tables the hub-status guard reads.
55
+ export function isHubDoc(doc) {
56
+ return isSprintHub(doc) || isCoordinationHub(doc);
57
+ }
58
+
59
+ // The row→target anchor: the first `.md` link in a table row is the plan that
60
+ // row is ABOUT. Shared by next-pickup detection (`detectBodyRunlistRefs`) and
61
+ // the hub-status guard, so the two can never disagree about which plan a given
62
+ // row names.
63
+ const ROW_LINK_RE = /\[[^\]]+\]\(([^)]+\.md(?:#[^)]+)?)\)/;
64
+
65
+ export function firstRowLink(line) {
66
+ const match = ROW_LINK_RE.exec(line);
67
+ return match ? match[1] : null;
68
+ }
69
+
70
+ // ─── Reading the status word out of a hub row ──────────────────────────────
71
+ //
72
+ // A hub row prints its child's status by hand. Two ways to find that word:
73
+ //
74
+ // position — strip HTML comments, take the cell's LEADING token, match it
75
+ // against the vocabulary the child's own type declares. No marker
76
+ // needed, so this works on tables that already exist. Measured on
77
+ // 472 real rows: agrees with an explicitly marked span 96% of the
78
+ // time and picks a wrong token zero times. Every miss leaves a
79
+ // leading word outside the vocabulary, so it declines (and the row
80
+ // is reported as unreadable) rather than rewriting confidently.
81
+ // marker — `<!--s-->active<!--/s-->` pins the span when position can't find
82
+ // it (the measured 4%: a status sitting behind a bolded headline).
83
+ //
84
+ // The marker is 19 characters, shorter than runlist's block grammar
85
+ // (`<!-- GENERATED:runlist:start -->`). That difference only matters because this
86
+ // one recurs hundreds of times per estate. It makes four comment grammars in
87
+ // runlist; the other three are block-level and correct at block level. Do NOT
88
+ // "unify" them, and do not let the count grow again.
89
+ export const MARKER_OPEN = '<!--s-->';
90
+ export const MARKER_CLOSE = '<!--/s-->';
91
+
92
+ const MARKED_SPAN_RE = /<!--\s*s\s*-->([\s\S]*?)<!--\s*\/s\s*-->/;
93
+ const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
94
+ const FENCE_RE = /^\s{0,3}(`{3,}|~{3,})/;
95
+ // Statuses are hyphenated single words (`in-session`, `queued-after`). The
96
+ // optional emphasis prefix lets `**active**` read as a leading token while
97
+ // `**Phase 1 gate** — active` still declines: the leading word there is `Phase`.
98
+ const LEADING_TOKEN_RE = /^(\s*)(\*\*|__|\*|_|`)?([A-Za-z][A-Za-z0-9-]*)/;
99
+
100
+ // Split a markdown table row into cells, keeping each cell's offsets into the
101
+ // line so a fix can rewrite one word without touching the rest of the row.
102
+ // Honors `\|` escapes and pipes inside inline code.
103
+ export function splitRowCells(line) {
104
+ const cells = [];
105
+ let start = 0;
106
+ let inCode = false;
107
+ for (let i = 0; i < line.length; i++) {
108
+ const ch = line[i];
109
+ if (ch === '\\') { i++; continue; }
110
+ if (ch === '`') { inCode = !inCode; continue; }
111
+ if (ch === '|' && !inCode) {
112
+ cells.push({ raw: line.slice(start, i), start, end: i });
113
+ start = i + 1;
114
+ }
115
+ }
116
+ cells.push({ raw: line.slice(start), start, end: line.length });
117
+ // Rows are conventionally pipe-delimited on both ends; drop the empty edge
118
+ // segments those produce so column indexes line up with the header's.
119
+ if (cells.length && cells[0].raw.trim() === '') cells.shift();
120
+ if (cells.length && cells[cells.length - 1].raw.trim() === '') cells.pop();
121
+ return cells;
122
+ }
123
+
124
+ function isDelimiterRow(cells) {
125
+ return cells.length > 0 && cells.every(cell => /^\s*:?-+:?\s*$/.test(cell.raw));
126
+ }
127
+
128
+ // A table declares a status column when a header cell IS "status"/"state".
129
+ // Deliberately exact (after stripping emphasis): a header that only mentions
130
+ // status in passing is not a promise that the column holds one.
131
+ function isStatusHeader(raw) {
132
+ const normalized = raw.replace(/[*_`]/g, '').trim().toLowerCase();
133
+ return normalized === 'status' || normalized === 'state';
134
+ }
135
+
136
+ export function findMarkedSpan(line) {
137
+ const match = MARKED_SPAN_RE.exec(line);
138
+ if (!match) return null;
139
+ const innerStart = match.index + match[0].indexOf('-->') + 3;
140
+ const inner = match[1];
141
+ const lead = inner.length - inner.trimStart().length;
142
+ const text = inner.trim();
143
+ if (!text) return null;
144
+ return { start: innerStart + lead, end: innerStart + lead + text.length, text, marked: true };
145
+ }
146
+
147
+ // Positional read of a cell: leading token, comments stripped, vocabulary-gated.
148
+ // `isKnownStatus` is the caller's vocabulary test — it depends on the CHILD's
149
+ // type, which only the caller can resolve, so it is injected rather than
150
+ // guessed here. Returns line-absolute offsets, like `findMarkedSpan`.
151
+ export function readPositionalToken(cell, isKnownStatus) {
152
+ if (!cell) return null;
153
+ // Blank out comments instead of deleting them so offsets stay line-absolute.
154
+ const masked = cell.raw.replace(HTML_COMMENT_RE, comment => ' '.repeat(comment.length));
155
+ const match = LEADING_TOKEN_RE.exec(masked);
156
+ if (!match) return null;
157
+ const word = match[3];
158
+ if (!isKnownStatus(word)) return null;
159
+ const start = cell.start + match[1].length + (match[2]?.length ?? 0);
160
+ return { start, end: start + word.length, text: word, marked: false };
161
+ }
162
+
163
+ // Walk a hub body's markdown tables and return one entry per row that names a
164
+ // child (`.md` link). Pure: no vocabulary, no resolution, no IO — the caller
165
+ // resolves the target and then reads the status word, because the vocabulary
166
+ // depends on the child's own type.
167
+ //
168
+ // Returns [{ lineIndex, ref, hasStatusColumn, statusCell, marked }] where
169
+ // `lineIndex` is 0-based into `body.split('\n')` and `statusCell`/`marked`
170
+ // carry line-absolute offsets.
171
+ export function scanHubStatusRows(body) {
172
+ if (!body) return [];
173
+ const lines = body.split('\n');
174
+ const rows = [];
175
+ let fence = null;
176
+ let table = null;
177
+
178
+ for (let i = 0; i < lines.length; i++) {
179
+ const line = lines[i];
180
+ const fenceMatch = FENCE_RE.exec(line);
181
+ if (fence) {
182
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) fence = null;
183
+ continue;
184
+ }
185
+ // Fenced examples routinely contain sample hub rows (this plan's own body
186
+ // does). Reading them would report drift against a fictional child.
187
+ if (fenceMatch) { fence = fenceMatch[1]; table = null; continue; }
188
+
189
+ if (!line.trim().startsWith('|')) { table = null; continue; }
190
+ const cells = splitRowCells(line);
191
+
192
+ if (!table) {
193
+ // A pipe line is a table header only when a delimiter row follows it.
194
+ const next = i + 1 < lines.length ? lines[i + 1] : '';
195
+ if (next.trim().startsWith('|') && isDelimiterRow(splitRowCells(next))) {
196
+ table = { statusColumn: cells.findIndex(cell => isStatusHeader(cell.raw)) };
197
+ }
198
+ continue;
199
+ }
200
+ if (isDelimiterRow(cells)) continue;
201
+
202
+ const ref = firstRowLink(line);
203
+ if (!ref) continue;
204
+ rows.push({
205
+ lineIndex: i,
206
+ ref,
207
+ hasStatusColumn: table.statusColumn >= 0,
208
+ statusCell: table.statusColumn >= 0 ? cells[table.statusColumn] ?? null : null,
209
+ marked: findMarkedSpan(line),
210
+ });
211
+ }
212
+ return rows;
213
+ }
214
+
215
+ // Write `replacement` in the case style the author used, so a fix never
216
+ // restyles a table (`Active` stays capitalized, `ACTIVE` stays shouted).
217
+ export function applyStatusCase(sample, replacement) {
218
+ if (/[A-Z]/.test(sample) && sample === sample.toUpperCase()) return replacement.toUpperCase();
219
+ if (/^[A-Z]/.test(sample)) return replacement.charAt(0).toUpperCase() + replacement.slice(1);
220
+ return replacement;
221
+ }
222
+
223
+ // Extract ordered plan refs from a hub's body prose. Two shapes:
224
+ // - link-list sections (`## Order of operations`, `## Runlist`, …) — every
225
+ // `.md` link or checklist item, in document order.
226
+ // - ranked-queue tables (`## Ranked queue`, …) — the first `.md` link in each
227
+ // table row (the ranked plan); header/separator rows contribute none.
228
+ // Coordination hubs encode their next-pickup order in the table shape; sprint-
229
+ // ish hubs use the link list. Deduped, first occurrence wins, order preserved.
230
+ //
231
+ // This is a hub's BODY MEMBERSHIP claim, not merely a set of links: it is the
232
+ // order `runlist runlist <hub>` / `runlist next <hub>` walk, so a plan listed here
233
+ // is one this hub would hand a session. The membership guard leans on exactly
234
+ // that — a plan in some other table is a pointer, a plan in this order is a claim.
235
+ export function detectBodyRunlistRefs(body) {
236
+ if (!body) return [];
237
+ const refs = [];
238
+ const linkRe = /\[[^\]]+\]\(([^)]+\.md(?:#[^)]+)?)\)/;
239
+ const sliceSection = (start) => {
240
+ const rest = body.slice(start);
241
+ const next = rest.search(/^##\s+/m);
242
+ return next >= 0 ? rest.slice(0, next) : rest;
243
+ };
244
+
245
+ const linkSectionRe = /^##\s+(?:Order of operations|Runlist|Execution order|Implementation order|Plan order)\b.*$/gim;
246
+ let match;
247
+ while ((match = linkSectionRe.exec(body)) !== null) {
248
+ const section = sliceSection(match.index + match[0].length);
249
+ // Line by line, because the SHAPE decides what a link means, not the
250
+ // heading's name. A section titled `## Runlist index — by category` matches
251
+ // this heading list but is written as tables, and taking every link in it
252
+ // made each row's descriptive prose a membership claim: a row about plan A
253
+ // whose prose says "spawned children B, C" ranked B and C as the hub's own,
254
+ // then warned B and C for not naming the hub as parent. In a table, only the
255
+ // row's first link is the ranked plan — the same rule the ranked-queue
256
+ // branch below already applies.
257
+ const allLinks = new RegExp(linkRe.source, 'g');
258
+ for (const rawLine of section.split('\n')) {
259
+ const line = rawLine.trim();
260
+ if (line.startsWith('|')) {
261
+ const link = firstRowLink(line);
262
+ if (link) refs.push(link);
263
+ continue;
264
+ }
265
+ // One ranked item per line, first link wins — the same rule as a table
266
+ // row. A ranked item routinely carries commentary that links elsewhere
267
+ // ("Phase 7's open item closes on a route decision in [other-plan.md]"),
268
+ // and counting those made the hub claim plans it was only citing. A hub
269
+ // that means to rank two plans lists them on two lines.
270
+ allLinks.lastIndex = 0;
271
+ const link = allLinks.exec(line);
272
+ if (link) refs.push(link[1]);
273
+ }
274
+
275
+ const checklistRe = /^\s*[-*]\s+\[[ xX]\]\s+([^\s)]+\.md(?:#[^\s)]+)?)/gm;
276
+ let item;
277
+ while ((item = checklistRe.exec(section)) !== null) refs.push(item[1]);
278
+ }
279
+
280
+ // Ranked-queue tables: the first `.md` link per row is the ranked plan. A
281
+ // header (`| Rank | Plan | … |`) and separator (`|---|`) carry no link and are
282
+ // skipped naturally. Heading may carry trailing text (`## Ranked queue (next
283
+ // pickup)`), so match the leading words, not an exact line.
284
+ const queueSectionRe = /^##\s+(?:Ranked queue|Queue|Pickup order|Heads)\b.*$/gim;
285
+ while ((match = queueSectionRe.exec(body)) !== null) {
286
+ const section = sliceSection(match.index + match[0].length);
287
+ for (const rawLine of section.split('\n')) {
288
+ const line = rawLine.trim();
289
+ if (!line.startsWith('|')) continue;
290
+ const link = firstRowLink(line);
291
+ if (link) refs.push(link);
292
+ }
293
+ }
294
+
295
+ return [...new Set(refs)];
296
+ }