@plot-pm/board 0.3.0 → 0.3.1-rc.1

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": "@plot-pm/board",
3
- "version": "0.3.0",
3
+ "version": "0.3.1-rc.1",
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",
package/plot-plan-meta.sh CHANGED
@@ -62,6 +62,10 @@
62
62
  # front matter `assignee:`; "" if absent
63
63
  # branches branch names from the `## Branches` section (backtick-
64
64
  # quoted, matching the known prefixes; sorted, unique)
65
+ # NOTE: per-branch annotations (`<!-- deferred: ... -->`,
66
+ # `<!-- claimed: ... -->`) bind to the LINE carrying the
67
+ # backticked branch name. An annotation on a wrapped
68
+ # continuation line is not seen — keep it on the branch line.
65
69
  # prs PR numbers from `→ #NNN` links in the `## Branches`
66
70
  # section (sorted, unique)
67
71
  # review_raw the plan's review-channel answer as written (`## Status`
@@ -72,6 +76,10 @@
72
76
  # impl normalized: own-branches|same-branch|other-repo|none|
73
77
  # UNKNOWN|NONE ("nowhere" is accepted for none; free-text
74
78
  # like "here, own branches" normalizes by token)
79
+ # released_raw the release transition record as written (`## Status`
80
+ # `Released:` or front matter `released:` — date and version).
81
+ # Empty until /plot-release records it; the tag stays the git
82
+ # truth and this merely reflects it.
75
83
  # approved_raw the approval transition record as written (`## Status`
76
84
  # `Approved:` or front matter `approved:` — who/when/channel,
77
85
  # e.g. "2026-07-30, alice, in-session"); "" if absent
@@ -107,7 +115,7 @@ if [ ${#files[@]} -eq 0 ] && [ ${#missing[@]} -eq 0 ]; then
107
115
  fi
108
116
 
109
117
  for f in ${missing[@]+"${missing[@]}"}; do
110
- 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":"","started_raw":[]}\n' \
118
+ 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":"","started_raw":[]}\n' \
111
119
  "$(printf '%s' "$f" | sed 's/\\/\\\\/g; s/"/\\"/g')"
112
120
  done
113
121
 
@@ -179,14 +187,16 @@ function norm_impl(raw, s) {
179
187
  function reset_state() {
180
188
  fm_status = ""; fm_phase = ""; fm_type = ""
181
189
  fm_title = ""; fm_sprint = ""; fm_story = ""; fm_assignee = ""
182
- fm_review = ""; fm_impl = ""; fm_approved = ""; fm_started = ""
190
+ fm_review = ""; fm_impl = ""; fm_approved = ""; fm_started = ""; fm_released = ""
183
191
  canon_phase = ""; canon_type = ""
184
192
  canon_sprint = ""; canon_story = ""; canon_assignee = ""
185
- canon_review = ""; canon_impl = ""; canon_approved = ""
193
+ canon_review = ""; canon_impl = ""; canon_approved = ""; canon_released = ""
186
194
  h1_title = ""
187
- in_fm = 0; section = ""; in_comment = 0
195
+ in_fm = 0; section = ""; in_comment = 0; branches_seen = 0
188
196
  delete branches; n_branches = 0
189
197
  delete prs; n_prs = 0
198
+ delete wave_names; delete wave_of; delete wave_seq; delete wave_count
199
+ delete deferred_of; delete claimed_of; delete ordered_b; n_waves = 0
190
200
  delete started; n_started = 0
191
201
  }
192
202
  function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, review, impl, approved, i, j, out, sorted_b, sorted_p, nb, np) {
@@ -209,6 +219,7 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
209
219
  review = strip_placeholder((fm_review != "") ? fm_review : canon_review)
210
220
  impl = strip_placeholder((fm_impl != "") ? fm_impl : canon_impl)
211
221
  approved = strip_placeholder((fm_approved != "") ? fm_approved : canon_approved)
222
+ released = strip_placeholder((fm_released != "") ? fm_released : canon_released)
212
223
  if (fm_started != "" && strip_placeholder(fm_started) != "") started[++n_started] = fm_started
213
224
  # Insertion sort + dedupe (portable: no gawk asort).
214
225
  nb = 0
@@ -236,9 +247,25 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
236
247
  out = out "],\"prs\":["
237
248
  for (i = 1; i <= np; i++) out = out (i > 1 ? "," : "") sorted_p[i]
238
249
  out = out "]"
250
+ # waves[]: branches grouped by `### ` subheading, in document order. A plan
251
+ # with no subheadings yields one wave with an empty name.
252
+ out = out ",\"waves\":["
253
+ for (w = 1; w <= n_waves; w++) {
254
+ out = out (w > 1 ? "," : "") "{\"name\":\"" jesc(wave_names[w]) "\",\"branches\":["
255
+ first = 1
256
+ for (i = 1; i <= n_branches; i++) {
257
+ if (wave_of[i] != w) continue
258
+ out = out (first ? "" : ",") "{\"branch\":\"" jesc(ordered_b[i]) "\",\"deferred\":" deferred_of[i] \
259
+ ",\"claimed\":\"" jesc(claimed_of[i]) "\"}"
260
+ first = 0
261
+ }
262
+ out = out "]}"
263
+ }
264
+ out = out "]"
239
265
  out = out ",\"review_raw\":\"" jesc(review) "\",\"review\":\"" norm_review(review) "\""
240
266
  out = out ",\"impl_raw\":\"" jesc(impl) "\",\"impl\":\"" norm_impl(impl) "\""
241
267
  out = out ",\"approved_raw\":\"" jesc(approved) "\""
268
+ out = out ",\"released_raw\":\"" jesc(released) "\""
242
269
  out = out ",\"started_raw\":["
243
270
  for (i = 1; i <= n_started; i++) out = out (i > 1 ? "," : "") "\"" jesc(started[i]) "\""
244
271
  out = out "]}"
@@ -264,6 +291,7 @@ in_fm {
264
291
  else if (lower ~ /^review:/ && fm_review == "") fm_review = val_after_colon($0)
265
292
  else if (lower ~ /^impl:/ && fm_impl == "") fm_impl = val_after_colon($0)
266
293
  else if (lower ~ /^approved:/ && fm_approved == "") fm_approved = val_after_colon($0)
294
+ else if (lower ~ /^released:/ && fm_released == "") fm_released = val_after_colon($0)
267
295
  else if (lower ~ /^started:/ && fm_started == "") fm_started = val_after_colon($0)
268
296
  next
269
297
  }
@@ -275,7 +303,9 @@ in_comment { if ($0 ~ /-->/) in_comment = 0; next }
275
303
  /^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
276
304
  /^## / {
277
305
  if ($0 ~ /^## Status/) section = "status"
278
- else if ($0 ~ /^## Branches/) section = "branches"
306
+ # First `## Branches` wins: a plan documenting the plan format quotes the
307
+ # section in prose, and those later headings are illustration, not contract.
308
+ else if ($0 ~ /^## Branches/) { section = branches_seen ? "" : "branches"; branches_seen = 1 }
279
309
  else if ($0 ~ /^## Approval/) section = "approval"
280
310
  else section = ""
281
311
  next
@@ -289,6 +319,7 @@ section == "status" {
289
319
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
290
320
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
291
321
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**approved[:*]/ && canon_approved == "") canon_approved = val_after_colon($0)
322
+ else if (lower ~ /^[ \t]*[-*]?[ \t]*\**released[:*]/ && canon_released == "") canon_released = val_after_colon($0)
292
323
  else if (lower ~ /^[ \t]*[-*]?[ \t]*\**started[:*]/) {
293
324
  _s = strip_placeholder(val_after_colon($0))
294
325
  if (_s != "") started[++n_started] = _s
@@ -301,10 +332,35 @@ section == "approval" {
301
332
  next
302
333
  }
303
334
  section == "branches" {
335
+ # `### <name>` opens a wave. Branches before any subheading belong to an
336
+ # unnamed wave, so a pre-wave plan parses as exactly one wave.
337
+ if ($0 ~ /^###[ \t]/) {
338
+ wave_names[++n_waves] = trim(substr($0, 4))
339
+ next
340
+ }
341
+ # Claim reflection, written by the worker after its ref push succeeds. This is
342
+ # a reflection, not the claim: git refs remain authoritative. Computed before
343
+ # the branch loop below — match() clobbers RSTART/RLENGTH, which that loop
344
+ # needs to advance.
345
+ claim_note = ""
346
+ if (index($0, "claimed:") > 0) {
347
+ _c = $0
348
+ sub(/^.*<!--[ \t]*claimed:[ \t]*/, "", _c)
349
+ sub(/[ \t]*-->.*$/, "", _c)
350
+ claim_note = trim(_c)
351
+ }
304
352
  line = $0
305
353
  while (match(line, branch_re)) {
306
354
  b = substr(line, RSTART + 1, RLENGTH - 2)
307
355
  branches[++n_branches] = b
356
+ if (n_waves == 0) { wave_names[++n_waves] = "" }
357
+ wave_of[n_branches] = n_waves
358
+ wave_seq[n_branches] = ++wave_count[n_waves]
359
+ deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred:/) ? "true" : "false"
360
+ # Claim reflection, written by the worker after its ref push succeeds. This
361
+ # is a reflection, not the claim: git refs remain authoritative.
362
+ claimed_of[n_branches] = claim_note
363
+ ordered_b[n_branches] = b
308
364
  line = substr(line, RSTART + RLENGTH)
309
365
  }
310
366
  line = $0