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.
- package/LICENSE +21 -0
- package/README.md +1112 -6
- package/assets/app/app.css +283 -0
- package/assets/app/app.mjs +381 -0
- package/assets/app/block-editor.mjs +182 -0
- package/assets/app/block-model.mjs +52 -0
- package/assets/app/document-create.mjs +86 -0
- package/assets/app/document-lifecycle.mjs +36 -0
- package/assets/app/document-stages.mjs +22 -0
- package/assets/app/document-views.mjs +33 -0
- package/assets/app/document-yardstick.mjs +258 -0
- package/assets/app/editor-navigation.mjs +51 -0
- package/assets/app/filing-navigation.mjs +138 -0
- package/assets/app/git-changes.mjs +80 -0
- package/assets/app/git-commit.mjs +46 -0
- package/assets/app/index.html +70 -0
- package/assets/app/library-navigation.mjs +93 -0
- package/assets/app/outline.mjs +18 -0
- package/assets/app/quick-navigation.mjs +52 -0
- package/assets/app/record-navigation.mjs +78 -0
- package/assets/app/recovery-center.mjs +53 -0
- package/assets/app/semantic-search.mjs +14 -0
- package/assets/app/settings.mjs +15 -0
- package/assets/app/shared.mjs +104 -0
- package/assets/app/stage-source.mjs +75 -0
- package/assets/app/template-editor.mjs +47 -0
- package/assets/app/theme.mjs +25 -0
- package/assets/app/transport.mjs +43 -0
- package/assets/contracts/source-editing-v1.md +232 -0
- package/assets/opencode/plugin.js +162 -0
- package/assets/schemas/runlist-record-v1.schema.json +912 -0
- package/bin/dotmd.mjs +5 -0
- package/bin/runlist.mjs +2714 -0
- package/package.json +53 -6
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
- package/runlist.config.example.mjs +370 -0
- package/scripts/postinstall.mjs +56 -0
- package/src/agent-context.mjs +201 -0
- package/src/ai.mjs +30 -0
- package/src/app-create.mjs +169 -0
- package/src/app-filing.mjs +139 -0
- package/src/app-git-commits.mjs +48 -0
- package/src/app-git.mjs +180 -0
- package/src/app-library.mjs +152 -0
- package/src/app-lifecycle-worker.mjs +12 -0
- package/src/app-lifecycle.mjs +79 -0
- package/src/app-records.mjs +134 -0
- package/src/app-search.mjs +80 -0
- package/src/app-semantic-search.mjs +105 -0
- package/src/app-service.mjs +208 -0
- package/src/app-yardstick.mjs +269 -0
- package/src/app.mjs +102 -0
- package/src/atomic-mutation.mjs +1736 -0
- package/src/baton.mjs +435 -0
- package/src/body-link.mjs +48 -0
- package/src/bulk-tag.mjs +174 -0
- package/src/check-collapse.mjs +75 -0
- package/src/claude-commands.mjs +75 -0
- package/src/cli-editor.mjs +8 -0
- package/src/code-refs.mjs +545 -0
- package/src/codex-integration.mjs +81 -0
- package/src/color.mjs +14 -0
- package/src/commands.mjs +374 -0
- package/src/completions.mjs +93 -0
- package/src/config-edit.mjs +606 -0
- package/src/config.mjs +705 -0
- package/src/decision-action.mjs +58 -0
- package/src/decision.mjs +220 -0
- package/src/decisions.mjs +787 -0
- package/src/deps.mjs +317 -0
- package/src/desktop-install.mjs +158 -0
- package/src/desktop-runtime.mjs +4 -0
- package/src/desktop.mjs +25 -0
- package/src/diff.mjs +101 -0
- package/src/doctor.mjs +591 -0
- package/src/durable-rename.mjs +55 -0
- package/src/errors-read.mjs +44 -0
- package/src/export.mjs +496 -0
- package/src/extractors.mjs +101 -0
- package/src/filing.mjs +255 -0
- package/src/fix-membership.mjs +134 -0
- package/src/fix-refs.mjs +174 -0
- package/src/flags.mjs +318 -0
- package/src/frontmatter-fix.mjs +195 -0
- package/src/frontmatter.mjs +252 -0
- package/src/git-commit-filters.mjs +32 -0
- package/src/git-commit-protocol.mjs +198 -0
- package/src/git-commit-signing.mjs +75 -0
- package/src/git-commit-store.mjs +26 -0
- package/src/git-commit-support.mjs +16 -0
- package/src/git-commit-worker.mjs +32 -0
- package/src/git.mjs +909 -0
- package/src/glossary-check.mjs +35 -0
- package/src/glossary.mjs +243 -0
- package/src/graph.mjs +298 -0
- package/src/guard.mjs +488 -0
- package/src/health.mjs +212 -0
- package/src/hints.mjs +129 -0
- package/src/host-integration.mjs +339 -0
- package/src/hub-membership.mjs +284 -0
- package/src/hub.mjs +296 -0
- package/src/hud.mjs +436 -0
- package/src/index-file.mjs +163 -0
- package/src/index.mjs +478 -0
- package/src/init.mjs +512 -0
- package/src/install.mjs +222 -0
- package/src/journal-read.mjs +116 -0
- package/src/journal.mjs +472 -0
- package/src/lifecycle.mjs +1444 -0
- package/src/lint.mjs +386 -0
- package/src/managed-path.mjs +192 -0
- package/src/markdown-code-spans.mjs +58 -0
- package/src/migrate-prompts.mjs +171 -0
- package/src/migrate-template.mjs +170 -0
- package/src/migrate.mjs +90 -0
- package/src/misuse-read.mjs +61 -0
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/modules.mjs +355 -0
- package/src/naming.mjs +55 -0
- package/src/native-action.mjs +59 -0
- package/src/native-record.mjs +244 -0
- package/src/new.mjs +1321 -0
- package/src/output-identity.mjs +111 -0
- package/src/parent-plan.mjs +58 -0
- package/src/parse-cache.mjs +99 -0
- package/src/path-containment.mjs +15 -0
- package/src/pickup-card.mjs +293 -0
- package/src/pickup.mjs +671 -0
- package/src/prompt.mjs +25 -0
- package/src/prompts.mjs +585 -0
- package/src/public-inspection.mjs +19 -0
- package/src/query.mjs +977 -0
- package/src/record-action.mjs +11 -0
- package/src/record-create.mjs +82 -0
- package/src/reference-planner.mjs +388 -0
- package/src/reference-values.mjs +11 -0
- package/src/rename.mjs +107 -0
- package/src/render.mjs +678 -0
- package/src/roadmap.mjs +184 -0
- package/src/runlist.mjs +944 -0
- package/src/section.mjs +225 -0
- package/src/semantic-search-client.mjs +58 -0
- package/src/ship.mjs +166 -0
- package/src/show.mjs +101 -0
- package/src/skill-drift.mjs +98 -0
- package/src/source-editor.mjs +491 -0
- package/src/stages.mjs +73 -0
- package/src/state-migration.mjs +123 -0
- package/src/stats.mjs +200 -0
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +721 -0
- package/src/summary.mjs +74 -0
- package/src/surfaces.mjs +28 -0
- package/src/sync-status.mjs +316 -0
- package/src/template-store.mjs +62 -0
- package/src/update.mjs +338 -0
- package/src/use.mjs +58 -0
- package/src/util.mjs +337 -0
- package/src/validate.mjs +795 -0
- package/src/watch.mjs +56 -0
- package/src/xref.mjs +406 -0
- package/src/yardstick-command.mjs +178 -0
- 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
|
+
}
|