@plot-pm/board 0.6.0-rc.2 → 0.6.0-rc.200
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/dist/board-server.mjs +88 -63
- package/package.json +2 -2
- package/plot-config.sh +18 -0
- package/plot-plan-meta.sh +131 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.6.0-rc.
|
|
3
|
+
"version": "0.6.0-rc.200",
|
|
4
4
|
"description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"build": "pnpm run build:client && pnpm run build:server",
|
|
33
33
|
"prepack": "pnpm run build",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
|
-
"test": "node --test test/*.test.mjs",
|
|
35
|
+
"test": "node --test --test-concurrency=4 test/*.test.mjs",
|
|
36
36
|
"test:integration": "pnpm build && vitest run"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
package/plot-config.sh
CHANGED
|
@@ -40,6 +40,17 @@
|
|
|
40
40
|
# Approve command how the board runs `/plot-approve <slug>`; the prompt is
|
|
41
41
|
# appended as one argument. Absent = the board's Approve
|
|
42
42
|
# button renders disabled, naming this key as the fix.
|
|
43
|
+
# Idea command how the board runs `/plot-idea` on a tracker issue; the
|
|
44
|
+
# prompt is appended as one argument, and it names a FILE
|
|
45
|
+
# the board wrote (an issue body is free text from anyone
|
|
46
|
+
# who can file an issue, so no part of it is ever a shell
|
|
47
|
+
# word). REQUIRED for the issue row's `Create plan`
|
|
48
|
+
# action, unlike `Approve command`: approving has a
|
|
49
|
+
# script to fall back to, and creating a plan does not —
|
|
50
|
+
# every step of /plot-idea is judgement, and no script
|
|
51
|
+
# here can invoke a skill. Absent (or `none`) = the button
|
|
52
|
+
# refuses and names this key as the fix, rather than
|
|
53
|
+
# accepting the click and doing nothing.
|
|
43
54
|
# Plot 2 posture keys (repo-declared ceremony bounds; all optional):
|
|
44
55
|
# Plan PRs required | never | optional (never = hard gate)
|
|
45
56
|
# Implementation home this repo | <repo/path list> | none
|
|
@@ -47,6 +58,13 @@
|
|
|
47
58
|
# Tracker plot | jira | github-issues | linear (+ URL)
|
|
48
59
|
# (plot = plans in this repo ARE the tracker; absent = same)
|
|
49
60
|
# Git host github | bitbucket (resolves gh vs bb)
|
|
61
|
+
# CI jenkins | github-actions | none — which CI system this
|
|
62
|
+
# project uses. Recorded by /plot-board-setup; not yet
|
|
63
|
+
# read by the board.
|
|
64
|
+
# Jenkins instance the slug or URL passed to a Jenkins CLI's -I flag.
|
|
65
|
+
# Read back by /plot-board-setup to verify auth against
|
|
66
|
+
# the right instance — without it the only runnable
|
|
67
|
+
# check verifies nothing.
|
|
50
68
|
#
|
|
51
69
|
# `Plan template` is a repo-root-relative path to the plan template /plot-idea
|
|
52
70
|
# instantiates; when absent, /plot-idea falls back to the shipped template.
|
package/plot-plan-meta.sh
CHANGED
|
@@ -45,8 +45,14 @@
|
|
|
45
45
|
# file the path given
|
|
46
46
|
# format canonical | frontmatter | none
|
|
47
47
|
# phase_raw primary phase value as written ("" if absent)
|
|
48
|
-
# phase normalized: draft|approved|delivered|released|
|
|
49
|
-
# superseded|UNKNOWN|NONE
|
|
48
|
+
# phase normalized: draft|design|approved|delivered|released|
|
|
49
|
+
# rejected|superseded|UNKNOWN|NONE
|
|
50
|
+
# `design` is a phase of its own, not a synonym for anything:
|
|
51
|
+
# a plan in Design cannot yet be handed to development because
|
|
52
|
+
# it needs a spec, a spike or a tracer bullet first. That is a
|
|
53
|
+
# different statement from "approved and nobody has started",
|
|
54
|
+
# which is a queue. (`ready-for-review`/`in-review` DO
|
|
55
|
+
# normalize onto `approved` — those are synonyms; this is not.)
|
|
50
56
|
# phase_alt_raw secondary value when the file carries two (front matter
|
|
51
57
|
# status: AND phase:), else ""
|
|
52
58
|
# phase_alt normalized phase_alt_raw (NONE when absent)
|
|
@@ -68,6 +74,27 @@
|
|
|
68
74
|
# continuation line is not seen — keep it on the branch line.
|
|
69
75
|
# prs PR numbers from `→ #NNN` links in the `## Branches`
|
|
70
76
|
# section (sorted, unique)
|
|
77
|
+
# changelog the plan's `## Changelog` entries, one string per bullet, in
|
|
78
|
+
# document order; [] when the plan has no changelog or an
|
|
79
|
+
# unfilled one. This is the one field that says WHAT A PLAN
|
|
80
|
+
# CHANGES, which title and story do not.
|
|
81
|
+
# ENTRIES, not lines: a bullet wrapped across several lines is
|
|
82
|
+
# one entry with the continuation lines joined by a single
|
|
83
|
+
# space (9 of the 34 changelogs in the origin repo wrap, so
|
|
84
|
+
# line-per-line would have shredded a quarter of them). An
|
|
85
|
+
# INDENTED bullet folds into the entry above it the same way:
|
|
86
|
+
# a sub-point is not a release note of its own.
|
|
87
|
+
# Non-bullet prose in the section is NOT an entry — 8 plans
|
|
88
|
+
# close their changelog with a "Board impact:" paragraph, which
|
|
89
|
+
# is a note to a reviewer rather than a release note. Comment
|
|
90
|
+
# interiors stay non-content, as everywhere else in this parser,
|
|
91
|
+
# so the template's guidance block contributes nothing.
|
|
92
|
+
# issues tracker issue numbers this plan answers, from the `## Status`
|
|
93
|
+
# `Issue:` line or front matter `issue:` (sorted, unique).
|
|
94
|
+
# A DEDICATED field, never a scan of the body for `#NNN`: a
|
|
95
|
+
# body scan cannot tell a signal from a citation, which is the
|
|
96
|
+
# same reason `prs` reads only `→ #NNN`. Accepts a list
|
|
97
|
+
# (`Issue: #226, #228`) because one plan can answer several.
|
|
71
98
|
# review_raw the plan's review-channel answer as written (`## Status`
|
|
72
99
|
# `Review:` or front matter `review:`); "" if absent
|
|
73
100
|
# review normalized: pr|in-session|ballot|UNKNOWN|NONE
|
|
@@ -80,6 +107,12 @@
|
|
|
80
107
|
# `Released:` or front matter `released:` — date and version).
|
|
81
108
|
# Empty until /plot-release records it; the tag stays the git
|
|
82
109
|
# truth and this merely reflects it.
|
|
110
|
+
# design_raw the design transition record as written (`## Status`
|
|
111
|
+
# `Design:` or front matter `design:` — who/when, and what the
|
|
112
|
+
# design work is); "" if absent. Reported for any plan that
|
|
113
|
+
# carries the line, whatever its phase: a plan that WENT
|
|
114
|
+
# THROUGH design keeps the record after moving on, exactly as
|
|
115
|
+
# `approved_raw` outlives the Approved phase.
|
|
83
116
|
# approved_raw the approval transition record as written (`## Status`
|
|
84
117
|
# `Approved:` or front matter `approved:` — who/when/channel,
|
|
85
118
|
# e.g. "2026-07-30, alice, in-session"); "" if absent
|
|
@@ -132,7 +165,7 @@ if [ ${#files[@]} -eq 0 ] && [ ${#missing[@]} -eq 0 ]; then
|
|
|
132
165
|
fi
|
|
133
166
|
|
|
134
167
|
for f in ${missing[@]+"${missing[@]}"}; do
|
|
135
|
-
printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[],"review_raw":"","review":"NONE","impl_raw":"","impl":"NONE","approved_raw":"","released_raw":"","delivered_raw":"","started_raw":[]}\n' \
|
|
168
|
+
printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[],"issues":[],"changelog":[],"review_raw":"","review":"NONE","impl_raw":"","impl":"NONE","design_raw":"","approved_raw":"","released_raw":"","delivered_raw":"","started_raw":[]}\n' \
|
|
136
169
|
"$(printf '%s' "$f" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
137
170
|
done
|
|
138
171
|
|
|
@@ -160,7 +193,7 @@ function norm_phase(raw, lower, toks, n, i, t) {
|
|
|
160
193
|
for (i = 1; i <= n; i++) {
|
|
161
194
|
t = toks[i]
|
|
162
195
|
gsub(/^[^a-z]+/, "", t); gsub(/[^a-z-]+$/, "", t)
|
|
163
|
-
if (t ~ /^(draft|approved|delivered|released|rejected|superseded)$/) return t
|
|
196
|
+
if (t ~ /^(draft|design|approved|delivered|released|rejected|superseded)$/) return t
|
|
164
197
|
if (t == "ready-for-review" || t == "in-review") return "approved"
|
|
165
198
|
}
|
|
166
199
|
return "UNKNOWN"
|
|
@@ -205,11 +238,11 @@ function reset_state() {
|
|
|
205
238
|
fm_status = ""; fm_phase = ""; fm_type = ""
|
|
206
239
|
fm_title = ""; fm_sprint = ""; fm_story = ""; fm_assignee = ""
|
|
207
240
|
fm_review = ""; fm_impl = ""; fm_approved = ""; fm_started = ""; fm_released = ""
|
|
208
|
-
fm_delivered = ""
|
|
241
|
+
fm_delivered = ""; fm_design = ""
|
|
209
242
|
canon_phase = ""; canon_type = ""
|
|
210
243
|
canon_sprint = ""; canon_story = ""; canon_assignee = ""
|
|
211
244
|
canon_review = ""; canon_impl = ""; canon_approved = ""; canon_released = ""
|
|
212
|
-
canon_delivered = ""
|
|
245
|
+
canon_delivered = ""; canon_design = ""
|
|
213
246
|
h1_title = ""
|
|
214
247
|
# "" means the plan carries no readable round — NOT zero. Emitted by omitting
|
|
215
248
|
# the field, so a consumer cannot mistake "never interrogated" for "asked
|
|
@@ -218,11 +251,15 @@ function reset_state() {
|
|
|
218
251
|
in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; branches_seen = 0
|
|
219
252
|
delete branches; n_branches = 0
|
|
220
253
|
delete prs; n_prs = 0
|
|
254
|
+
fm_issue = ""; canon_issue = ""
|
|
255
|
+
delete issues; n_issues = 0
|
|
221
256
|
delete wave_names; delete wave_of; delete wave_seq; delete wave_count
|
|
222
257
|
delete deferred_of; delete claimed_of; delete ordered_b; n_waves = 0
|
|
223
258
|
delete started; n_started = 0
|
|
259
|
+
fm_changelog = ""
|
|
260
|
+
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
224
261
|
}
|
|
225
|
-
function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, review, impl, approved, delivered, i, j, out, sorted_b, sorted_p, nb, np) {
|
|
262
|
+
function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, review, impl, design, approved, delivered, issue, i, j, out, sorted_b, sorted_p, sorted_i, nb, np, ni) {
|
|
226
263
|
if (fm_status != "" || fm_phase != "") {
|
|
227
264
|
fmt = "frontmatter"
|
|
228
265
|
praw = (fm_status != "") ? fm_status : fm_phase
|
|
@@ -241,10 +278,25 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
241
278
|
assignee = strip_placeholder((fm_assignee != "") ? fm_assignee : canon_assignee)
|
|
242
279
|
review = strip_placeholder((fm_review != "") ? fm_review : canon_review)
|
|
243
280
|
impl = strip_placeholder((fm_impl != "") ? fm_impl : canon_impl)
|
|
281
|
+
design = strip_placeholder((fm_design != "") ? fm_design : canon_design)
|
|
244
282
|
approved = strip_placeholder((fm_approved != "") ? fm_approved : canon_approved)
|
|
245
283
|
released = strip_placeholder((fm_released != "") ? fm_released : canon_released)
|
|
246
284
|
delivered = strip_placeholder((fm_delivered != "") ? fm_delivered : canon_delivered)
|
|
247
285
|
if (fm_started != "" && strip_placeholder(fm_started) != "") started[++n_started] = fm_started
|
|
286
|
+
# `Issue:` names the tracker signals this plan answers, and it is a DEDICATED
|
|
287
|
+
# field rather than a scan of the body for `#NNN`. A body scan cannot tell a
|
|
288
|
+
# signal from a citation: plans in this repo mention PR numbers constantly,
|
|
289
|
+
# and one of them cites `#226`, `#227`, `#228` as history in its Motivation
|
|
290
|
+
# while naming `PR #232` two sections later. That is the same ambiguity `prs`
|
|
291
|
+
# already answered by reading only `-> #NNN` — one field, one meaning.
|
|
292
|
+
#
|
|
293
|
+
# A LIST, because one plan can answer several signals; the plan that
|
|
294
|
+
# introduced this field subsumes three.
|
|
295
|
+
issue = strip_placeholder((fm_issue != "") ? fm_issue : canon_issue)
|
|
296
|
+
while (match(issue, /#[0-9]+/)) {
|
|
297
|
+
issues[++n_issues] = substr(issue, RSTART + 1, RLENGTH - 1)
|
|
298
|
+
issue = substr(issue, RSTART + RLENGTH)
|
|
299
|
+
}
|
|
248
300
|
# Insertion sort + dedupe (portable: no gawk asort).
|
|
249
301
|
nb = 0
|
|
250
302
|
for (i = 1; i <= n_branches; i++) {
|
|
@@ -260,6 +312,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
260
312
|
for (j = np; j >= 1 && sorted_p[j] > prs[i]+0; j--) sorted_p[j+1] = sorted_p[j]
|
|
261
313
|
sorted_p[j+1] = prs[i]+0; np++
|
|
262
314
|
}
|
|
315
|
+
ni = 0
|
|
316
|
+
for (i = 1; i <= n_issues; i++) {
|
|
317
|
+
for (j = 1; j <= ni && sorted_i[j] != issues[i]+0; j++) ;
|
|
318
|
+
if (j <= ni) continue
|
|
319
|
+
for (j = ni; j >= 1 && sorted_i[j] > issues[i]+0; j--) sorted_i[j+1] = sorted_i[j]
|
|
320
|
+
sorted_i[j+1] = issues[i]+0; ni++
|
|
321
|
+
}
|
|
263
322
|
out = "{\"file\":\"" jesc(cur_file) "\",\"format\":\"" fmt "\""
|
|
264
323
|
out = out ",\"phase_raw\":\"" jesc(praw) "\",\"phase\":\"" norm_phase(praw) "\""
|
|
265
324
|
out = out ",\"phase_alt_raw\":\"" jesc(palt_raw) "\",\"phase_alt\":\"" norm_phase(palt_raw) "\""
|
|
@@ -270,6 +329,17 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
270
329
|
for (i = 1; i <= nb; i++) out = out (i > 1 ? "," : "") "\"" jesc(sorted_b[i]) "\""
|
|
271
330
|
out = out "],\"prs\":["
|
|
272
331
|
for (i = 1; i <= np; i++) out = out (i > 1 ? "," : "") sorted_p[i]
|
|
332
|
+
out = out "],\"issues\":["
|
|
333
|
+
for (i = 1; i <= ni; i++) out = out (i > 1 ? "," : "") sorted_i[i]
|
|
334
|
+
out = out "]"
|
|
335
|
+
# Document order, never sorted: a changelog is a narrative sequence, and the
|
|
336
|
+
# first entry is the headline. Front matter wins, as it does for every other
|
|
337
|
+
# field, and contributes exactly one entry.
|
|
338
|
+
if (fm_changelog != "" && strip_placeholder(fm_changelog) != "") {
|
|
339
|
+
delete changelog; n_changelog = 1; changelog[1] = strip_placeholder(fm_changelog)
|
|
340
|
+
}
|
|
341
|
+
out = out ",\"changelog\":["
|
|
342
|
+
for (i = 1; i <= n_changelog; i++) out = out (i > 1 ? "," : "") "\"" jesc(changelog[i]) "\""
|
|
273
343
|
out = out "]"
|
|
274
344
|
# waves[]: branches grouped by `### ` subheading, in document order. A plan
|
|
275
345
|
# with no subheadings yields one wave with an empty name.
|
|
@@ -288,6 +358,7 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
288
358
|
out = out "]"
|
|
289
359
|
out = out ",\"review_raw\":\"" jesc(review) "\",\"review\":\"" norm_review(review) "\""
|
|
290
360
|
out = out ",\"impl_raw\":\"" jesc(impl) "\",\"impl\":\"" norm_impl(impl) "\""
|
|
361
|
+
out = out ",\"design_raw\":\"" jesc(design) "\""
|
|
291
362
|
out = out ",\"approved_raw\":\"" jesc(approved) "\""
|
|
292
363
|
out = out ",\"released_raw\":\"" jesc(released) "\""
|
|
293
364
|
out = out ",\"delivered_raw\":\"" jesc(delivered) "\""
|
|
@@ -315,13 +386,19 @@ in_fm {
|
|
|
315
386
|
else if (lower ~ /^title:/ && fm_title == "") fm_title = val_after_colon($0)
|
|
316
387
|
else if (lower ~ /^sprint:/ && fm_sprint == "") fm_sprint = val_after_colon($0)
|
|
317
388
|
else if (lower ~ /^story:/ && fm_story == "") fm_story = val_after_colon($0)
|
|
389
|
+
else if (lower ~ /^issue:/ && fm_issue == "") fm_issue = val_after_colon($0)
|
|
318
390
|
else if (lower ~ /^assignee:/ && fm_assignee == "") fm_assignee = val_after_colon($0)
|
|
319
391
|
else if (lower ~ /^review:/ && fm_review == "") fm_review = val_after_colon($0)
|
|
320
392
|
else if (lower ~ /^impl:/ && fm_impl == "") fm_impl = val_after_colon($0)
|
|
393
|
+
else if (lower ~ /^design:/ && fm_design == "") fm_design = val_after_colon($0)
|
|
321
394
|
else if (lower ~ /^approved:/ && fm_approved == "") fm_approved = val_after_colon($0)
|
|
322
395
|
else if (lower ~ /^released:/ && fm_released == "") fm_released = val_after_colon($0)
|
|
323
396
|
else if (lower ~ /^delivered:/ && fm_delivered == "") fm_delivered = val_after_colon($0)
|
|
324
397
|
else if (lower ~ /^started:/ && fm_started == "") fm_started = val_after_colon($0)
|
|
398
|
+
# A scalar `changelog:` in front matter is one entry. A YAML list is NOT read
|
|
399
|
+
# here: front matter in this repo is a flat key/value surface, and guessing a
|
|
400
|
+
# list grammar the format never promised would invent a contract.
|
|
401
|
+
else if (lower ~ /^changelog:/ && fm_changelog == "") fm_changelog = val_after_colon($0)
|
|
325
402
|
next
|
|
326
403
|
}
|
|
327
404
|
# Interior of multi-line HTML comments is non-content (template guidance
|
|
@@ -359,6 +436,10 @@ in_comment {
|
|
|
359
436
|
# section in prose, and those later headings are illustration, not contract.
|
|
360
437
|
else if ($0 ~ /^## Branches/) { section = branches_seen ? "" : "branches"; branches_seen = 1 }
|
|
361
438
|
else if ($0 ~ /^## Approval/) section = "approval"
|
|
439
|
+
# First `## Changelog` wins, for the same reason `## Branches` does: a plan
|
|
440
|
+
# about the plan format quotes the section in prose, and the later heading is
|
|
441
|
+
# illustration rather than contract.
|
|
442
|
+
else if ($0 ~ /^## Changelog/) { section = changelog_seen ? "" : "changelog"; changelog_seen = 1 }
|
|
362
443
|
else section = ""
|
|
363
444
|
next
|
|
364
445
|
}
|
|
@@ -368,8 +449,10 @@ section == "status" {
|
|
|
368
449
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**type[:*]/ && canon_type == "") canon_type = val_after_colon($0)
|
|
369
450
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**sprint[:*]/ && canon_sprint == "") canon_sprint = val_after_colon($0)
|
|
370
451
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**story[:*]/ && canon_story == "") canon_story = val_after_colon($0)
|
|
452
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**issue[:*]/ && canon_issue == "") canon_issue = val_after_colon($0)
|
|
371
453
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
|
|
372
454
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
|
|
455
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**design[:*]/ && canon_design == "") canon_design = val_after_colon($0)
|
|
373
456
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = val_after_colon($0)
|
|
374
457
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = val_after_colon($0)
|
|
375
458
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**delivered[:*]/ && canon_delivered == "") canon_delivered = val_after_colon($0)
|
|
@@ -384,6 +467,47 @@ section == "approval" {
|
|
|
384
467
|
if (lower ~ /^[ \t]*[-*]?[ \t]*\**assignee[:*]/ && canon_assignee == "") canon_assignee = val_after_colon($0)
|
|
385
468
|
next
|
|
386
469
|
}
|
|
470
|
+
# `## Changelog` — the release-note entries. One entry per bullet, wrapped
|
|
471
|
+
# continuation lines folded into the entry they belong to.
|
|
472
|
+
#
|
|
473
|
+
# A bullet opens an entry. An indented non-bullet line continues the open one.
|
|
474
|
+
# A blank line or a line at column zero closes it: the flush-left prose that 8
|
|
475
|
+
# plans use for a "Board impact:" note is a remark to a reviewer, not a release
|
|
476
|
+
# note, so it ends the entry rather than joining it.
|
|
477
|
+
section == "changelog" {
|
|
478
|
+
# A line that is nothing but an HTML comment is not content, at any
|
|
479
|
+
# indentation. Multi-line comments are already swallowed upstream; an INDENTED
|
|
480
|
+
# single-liner would otherwise land in the continuation branch below and paste
|
|
481
|
+
# its own markup into the entry above it.
|
|
482
|
+
if ($0 ~ /^[ \t]*<!--.*-->[ \t]*$/) next
|
|
483
|
+
# An INDENTED bullet is a sub-point of the entry above, not a release note of
|
|
484
|
+
# its own, so it continues rather than opens. No changelog in the repo nests
|
|
485
|
+
# today; the rule is here because the alternative silently promotes a
|
|
486
|
+
# sub-point to a headline the moment one does.
|
|
487
|
+
if (cl_open && $0 ~ /^[ \t]+[-*][ \t]/) {
|
|
488
|
+
_n = $0
|
|
489
|
+
sub(/^[ \t]*[-*][ \t]+/, "", _n)
|
|
490
|
+
changelog[n_changelog] = changelog[n_changelog] " " trim(_n)
|
|
491
|
+
next
|
|
492
|
+
}
|
|
493
|
+
if ($0 ~ /^[ \t]*[-*][ \t]/) {
|
|
494
|
+
_e = $0
|
|
495
|
+
sub(/^[ \t]*[-*][ \t]+/, "", _e)
|
|
496
|
+
_e = strip_placeholder(trim(_e))
|
|
497
|
+
if (_e != "") { changelog[++n_changelog] = _e; cl_open = 1 }
|
|
498
|
+
else cl_open = 0 # an unfilled bullet opens nothing to continue
|
|
499
|
+
next
|
|
500
|
+
}
|
|
501
|
+
if (cl_open && $0 ~ /^[ \t]+[^ \t]/) {
|
|
502
|
+
changelog[n_changelog] = changelog[n_changelog] " " trim($0)
|
|
503
|
+
next
|
|
504
|
+
}
|
|
505
|
+
# Blank line, or prose at column zero: whatever entry was open is finished.
|
|
506
|
+
# The flag matters — without it a continuation-shaped line after a break would
|
|
507
|
+
# glue itself onto an entry it never belonged to.
|
|
508
|
+
cl_open = 0
|
|
509
|
+
next
|
|
510
|
+
}
|
|
387
511
|
section == "branches" {
|
|
388
512
|
# `### <name>` opens a wave. Branches before any subheading belong to an
|
|
389
513
|
# unnamed wave, so a pre-wave plan parses as exactly one wave.
|