@plot-pm/board 0.7.0-rc.1 → 0.7.0-rc.10

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 (2) hide show
  1. package/package.json +1 -1
  2. package/plot-plan-meta.sh +135 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plot-pm/board",
3
- "version": "0.7.0-rc.1",
3
+ "version": "0.7.0-rc.10",
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
@@ -36,6 +36,27 @@
36
36
  # Front matter wins when both are present (it is the machine-facing surface).
37
37
  # A file with neither is reported as format "none" (pre-plot / legacy plan).
38
38
  #
39
+ # The IMPLEMENTATION section (which branches, in which waves, with which PRs)
40
+ # has TWO spellings, and this parser reads both:
41
+ #
42
+ # ## Branches (old) the branch rides the list line, meta mixed with prose:
43
+ # ### Removed
44
+ # - `bug/foo` — loses its half → #300
45
+ #
46
+ # ## Waves (new) the `### ` heading carries the meta, the line is prose:
47
+ # ### Removed (Branch: bug/foo, PR: #300)
48
+ # - loses its half
49
+ #
50
+ # Both emit the SAME branches/prs/waves arrays. The new shape is the format Plot
51
+ # writes and documents; the old one is kept readable because a format change owes
52
+ # its estate a migration that moves files one at a time, and a plan moved one
53
+ # commit before the parser learns the shape must not read as silently empty —
54
+ # the failure that makes a plan disappear from the fleet scan and pass the
55
+ # delivery gate. In the new shape the branch comes from the HEADING, so a
56
+ # backticked name in a description cannot be mistaken for a branch — the defect
57
+ # the old shape invited (a second path-shaped token on a line read as a phantom
58
+ # branch) is structurally impossible.
59
+ #
39
60
  # Phase values are normalized by scanning whitespace-separated tokens for the
40
61
  # first known phase word — so decorated real-world values like
41
62
  # "Delivered (2026-06-29) — split done" normalize to "delivered". A non-empty
@@ -66,8 +87,13 @@
66
87
  # front matter `story:`); "" if absent or a placeholder
67
88
  # assignee github handle from the `## Approval` `Assignee:` line or
68
89
  # front matter `assignee:`; "" if absent
69
- # branches branch names from the `## Branches` section (backtick-
70
- # quoted, matching the known prefixes; sorted, unique)
90
+ # branches branch names, sorted and unique, read from EITHER spelling:
91
+ # the old `## Branches` section (backtick-quoted in the list
92
+ # line, matching the known prefixes) OR the new `## Waves`
93
+ # section (`Branch:` in a `### ` heading — see below). Both
94
+ # spellings emit the same array; a plan carries one or the
95
+ # other, and the parser reads both so a migration that moves
96
+ # files one at a time never makes a plan silently empty.
71
97
  # NOTE: per-branch annotations (`<!-- deferred: ... -->`,
72
98
  # `<!-- claimed: ... -->`) bind to the LINE carrying the
73
99
  # backticked branch name. An annotation on a wrapped
@@ -75,10 +101,13 @@
75
101
  # `<!-- deferred -->` (bare, no colon) sets the flag with no
76
102
  # reason; `waves[].branches[].deferred_reason` carries the
77
103
  # sentence after the colon, "" where none was written.
78
- # prs PR numbers from `→ #NNN` and `→ owner/repo#NNN` links in
79
- # the `## Branches` section (sorted, unique). The repo part is
80
- # matched but not retained: callers ask which PRs are a plan's
81
- # evidence, and plot-host.sh resolves where each one lives.
104
+ # prs PR numbers, sorted and unique, read from EITHER spelling:
105
+ # `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
106
+ # section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
107
+ # repo part is matched but not retained: callers ask which PRs
108
+ # are a plan's evidence, and plot-host.sh resolves where each
109
+ # one lives. An absent PR contributes nothing — not "", not 0 —
110
+ # the same rule `Issue:` follows.
82
111
  # malformed_prs near-miss annotations, verbatim — currently `→#NNN` with no
83
112
  # space. Reported rather than dropped: "no annotation" is a
84
113
  # claim the sweep acts on, so a typo that reads as absence
@@ -258,7 +287,7 @@ function reset_state() {
258
287
  # the field, so a consumer cannot mistake "never interrogated" for "asked
259
288
  # nothing".
260
289
  rounds = ""
261
- in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; branches_seen = 0
290
+ in_fm = 0; section = ""; in_comment = 0; in_challenge = 0; in_fence = 0; branches_seen = 0; waves_seen = 0
262
291
  delete branches; n_branches = 0
263
292
  delete prs; n_prs = 0
264
293
  delete malformed_prs; n_malformed_prs = 0
@@ -442,6 +471,22 @@ in_comment {
442
471
  in_challenge = ($0 ~ /CHALLENGE-THE-PLAN-METADATA/) ? 1 : 0
443
472
  next
444
473
  }
474
+ # A fenced code block is illustration, never contract — the same standing rule
475
+ # comment interiors and repeated headings already follow. A plan that documents
476
+ # the plan format shows a `## Waves` or `## Branches` block inside a ``` fence,
477
+ # and those example headings must contribute no section, no branch and no PR.
478
+ #
479
+ # Measured need: waves-name-themselves shows its `## Waves` example in a fenced
480
+ # block whose `### Removed (Branch: bug/an-agent…)` headings are realistic. With
481
+ # no fence tracking the parser read them as real branches of the plan — and the
482
+ # same trick already fooled the OLD `## Branches` path (a fenced `## Branches`
483
+ # example won its first-heading-wins guard and hid the real section). Toggling on
484
+ # a fence fence-marker line closes both.
485
+ #
486
+ # A fence marker is a line whose first non-space run is ``` or ~~~ (an info
487
+ # string like ```markdown may follow). The marker line itself is never content.
488
+ /^[ \t]*(```|~~~)/ { in_fence = !in_fence; next }
489
+ in_fence { next }
445
490
  # First H1 is the title fallback (front matter title: still wins in emit).
446
491
  /^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
447
492
  /^## / {
@@ -449,6 +494,12 @@ in_comment {
449
494
  # First `## Branches` wins: a plan documenting the plan format quotes the
450
495
  # section in prose, and those later headings are illustration, not contract.
451
496
  else if ($0 ~ /^## Branches/) { section = branches_seen ? "" : "branches"; branches_seen = 1 }
497
+ # `## Waves` is the new spelling: the branch and PR live in the `### ` heading,
498
+ # the line below is prose. First one wins, for the same reason `## Branches`
499
+ # does. A plan carries one or the other — but the parser reads both while the
500
+ # migration moves 85 files, so a file moved one commit early never reads
501
+ # as silently empty.
502
+ else if ($0 ~ /^## Waves/) { section = waves_seen ? "" : "waves"; waves_seen = 1 }
452
503
  else if ($0 ~ /^## Approval/) section = "approval"
453
504
  # First `## Changelog` wins, for the same reason `## Branches` does: a plan
454
505
  # about the plan format quotes the section in prose, and the later heading is
@@ -613,5 +664,82 @@ section == "branches" {
613
664
  }
614
665
  next
615
666
  }
667
+ # `## Waves` — the new spelling. The `### ` heading carries the meta (which
668
+ # branch, which PR) and the line below is prose. This is the inverse of
669
+ # `## Branches`, where the branch rides the list line: here the branch comes
670
+ # from the HEADING, so a backticked name in a description cannot be mistaken for
671
+ # a branch — the defect the old shape invited is structurally impossible.
672
+ #
673
+ # The emitted arrays (branches, prs, waves) must be byte-identical to what the
674
+ # old shape produces for the same plan. So this shares every accumulation
675
+ # variable with the branches handler above; only the EXTRACTION differs.
676
+ section == "waves" {
677
+ # Only the `### ` heading carries meta. Body lines are prose — never scanned
678
+ # for a branch or a PR, which is the whole point of the new shape.
679
+ if ($0 !~ /^###[ \t]/) next
680
+
681
+ # A heading opens a wave whose NAME is the heading text with the
682
+ # `(Branch: …, PR: …)` parenthetical and any trailing annotation stripped, so
683
+ # `### Removed (Branch: bug/foo, PR: #300)` names the wave `Removed` — exactly
684
+ # what the old shape (bare `### Removed`) produced, which is what keeps the two
685
+ # spellings byte-identical.
686
+ wname = trim(substr($0, 4))
687
+ # Drop a trailing HTML comment (claimed/deferred ride the heading line now).
688
+ sub(/[ \t]*<!--.*$/, "", wname)
689
+ # Drop the `(Branch: …)` meta parenthetical. Anchored to `(Branch:` so a
690
+ # parenthetical in a genuine wave name — none exist, but the grammar must not
691
+ # assume it — is not eaten unless it is the meta block.
692
+ sub(/[ \t]*\(Branch:.*$/, "", wname)
693
+ wname = trim(wname)
694
+ wave_names[++n_waves] = wname
695
+
696
+ # Claim/deferral annotations bind to the line carrying the branch name, which
697
+ # is the heading. Read before any match() below, which clobbers RSTART/RLENGTH.
698
+ claim_note = ""
699
+ if (index($0, "claimed:") > 0) {
700
+ _c = $0
701
+ sub(/^.*<!--[ \t]*claimed:[ \t]*/, "", _c)
702
+ sub(/[ \t]*-->.*$/, "", _c)
703
+ claim_note = trim(_c)
704
+ }
705
+ defer_note = ""
706
+ if (index($0, "deferred:") > 0) {
707
+ _d = $0
708
+ sub(/^.*<!--[ \t]*deferred:[ \t]*/, "", _d)
709
+ sub(/[ \t]*-->.*$/, "", _d)
710
+ defer_note = trim(_d)
711
+ }
712
+
713
+ # The branch is the `Branch:` value, matched against the known prefixes exactly
714
+ # as the old shape matched the backticked name. Written unquoted in the heading
715
+ # (`Branch: bug/foo`), so the match is on a bare token, not on backticks. A
716
+ # heading with no readable branch still opened a wave above — so a `## Waves`
717
+ # section is never silently empty, which is the failure this plan refuses: a
718
+ # consumer sees a wave it could not extract a branch from, not an absence.
719
+ hmeta = $0
720
+ if (match(hmeta, "Branch:[ \t]*(" PREFIXES ")/[^ \t,)]+")) {
721
+ b = substr(hmeta, RSTART, RLENGTH)
722
+ sub(/^Branch:[ \t]*/, "", b)
723
+ branches[++n_branches] = b
724
+ wave_of[n_branches] = n_waves
725
+ wave_seq[n_branches] = ++wave_count[n_waves]
726
+ # Bare `<!-- deferred -->` sets the flag with no reason, same as the old
727
+ # shape; `deferred:` carries the reason.
728
+ deferred_of[n_branches] = ($0 ~ /<!--[ \t]*deferred[ \t]*(:|-->)/) ? "true" : "false"
729
+ deferred_why[n_branches] = defer_note
730
+ claimed_of[n_branches] = claim_note
731
+ ordered_b[n_branches] = b
732
+ }
733
+
734
+ # The PR is the `PR: #NNN` value in the heading. Absent PR: contributes
735
+ # nothing — not "", not 0 — the same rule `Issue:` follows. Only a heading
736
+ # that carries the field adds to prs.
737
+ if (match($0, /PR:[ \t]*#[0-9]+/)) {
738
+ p = substr($0, RSTART, RLENGTH)
739
+ sub(/^.*#/, "", p)
740
+ prs[++n_prs] = p
741
+ }
742
+ next
743
+ }
616
744
  END { if (NR > 0) emit_record() }
617
745
  ' "${files[@]}"