feat: record release window membership separately #239
1 changed files with 209 additions and 0 deletions
|
|
@ -2384,6 +2384,215 @@ check "...with the older carrier still asked for nothing" 1 "" \
|
|||
collision_flags_issue 257 \
|
||||
<<<$'257\tready\tissueflow-reconcile.sh + issueflow-reconcile.test.sh — one\n284\tready\tactions/issueflow-reconcile — two'
|
||||
|
||||
# -- a release carrier is not a member of its own gate (#327 D1) -----------
|
||||
self_gate_body='A member narrates Blocked by #163.'
|
||||
check "a self-only parsed gate does not make its release issue a carrier" 1 "" \
|
||||
grep -q . < <(release_window_gate 163 $'163\n164' <<<"$self_gate_body")
|
||||
mixed_gate_body='Blocked by #163, #164, #165.'
|
||||
check "an open non-self member still makes the release issue a carrier" 0 \
|
||||
$'163\t164\n163\t165' \
|
||||
release_window_gate 163 $'163\n164' <<<"$mixed_gate_body"
|
||||
# shellcheck disable=SC2016 # awk fields belong to awk, not the shell
|
||||
check "the carrier number never contributes to its own WINDOW_GATE" 1 "" \
|
||||
awk -F '\t' '$2 == 163 { found = 1 } END { exit !found }' \
|
||||
< <(release_window_gate 163 $'163\n164' <<<"$mixed_gate_body")
|
||||
|
||||
# -- the membership record, read by heading (#343 D2) -----------------------
|
||||
# The record is a machine record with one shape, and every case below is a
|
||||
# shape a real release body already carries. The corpus is heavy-duty/crew#346
|
||||
# — 21 members, a literal "## The members, in claim order" narration heading,
|
||||
# rows citing merged PRs and another repository, one row annotating an issue as
|
||||
# explicitly NOT a member, a verification lane that is not in the build queue,
|
||||
# and a "## Task list" progress view beside all of it.
|
||||
# Bracketed, so the assertion is the WHOLE set and not a prefix of it: a
|
||||
# substring match on a bare list would let an extra member in silently, which
|
||||
# is the one direction every mutation below travels.
|
||||
membership_set() { # release body on stdin -> its members as one bracketed line
|
||||
printf '[%s]\n' "$(membership_references | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
|
||||
}
|
||||
membership_body="$(printf '%s\n' \
|
||||
'The 0.6.0 window. Blocked by #249.' \
|
||||
'A quoted declaration in narration: "Blocked by #906" is what the epic says.' \
|
||||
'' \
|
||||
'## The members, in claim order' \
|
||||
'- #900 — a narration heading, not the record' \
|
||||
'' \
|
||||
'## Members' \
|
||||
'- [ ] #253 — landed as #901, ports heavy-duty/crew#346, and #902 is not a member' \
|
||||
'- #257' \
|
||||
'- [x] #264 — landed' \
|
||||
'- crew#348 — a parallel track in another repository' \
|
||||
'- #266, #276 — two references on one row' \
|
||||
'- the verification lane, not in the build queue' \
|
||||
'- #249 — the sink itself' \
|
||||
'' \
|
||||
'## Task list' \
|
||||
'- [ ] #281 — the progress view')"
|
||||
check "the record enrols exactly its rows' bare first tokens" 0 "[249 253 257 264]" \
|
||||
membership_set <<<"$membership_body"
|
||||
# The heading is ANCHORED. crew#346 carries this exact narration heading, so a
|
||||
# substring or prefix match reads it as the record and enrols its rows.
|
||||
check "a narrated members heading is not the record" 1 "" \
|
||||
grep -qx 900 < <(membership_references <<<"$membership_body")
|
||||
# The row's FIRST token, not every reference in it. epic_references prints the
|
||||
# whole row and takes them all, which is right for a progress view and wrong
|
||||
# here: these three are a merged PR, a sibling repository, and an issue the row
|
||||
# itself names as a non-member.
|
||||
check "a row's prose PR reference is not a member" 1 "" \
|
||||
grep -qx 901 < <(membership_references <<<"$membership_body")
|
||||
check "...nor an issue the row names as explicitly not a member" 1 "" \
|
||||
grep -qx 902 < <(membership_references <<<"$membership_body")
|
||||
check "...nor a qualified reference in the prose" 1 "" \
|
||||
grep -qx 346 < <(membership_references <<<"$membership_body")
|
||||
# Silence, not a guess: a first token that is not a bare local reference
|
||||
# contributes nothing, whether it is qualified, punctuated, or prose.
|
||||
check "a qualified first token contributes no member" 1 "" \
|
||||
grep -qx 348 < <(membership_references <<<"$membership_body")
|
||||
check "a punctuated first token contributes no member" 1 "" \
|
||||
grep -qx 266 < <(membership_references <<<"$membership_body")
|
||||
check "...and the second reference on that row contributes none either" 1 "" \
|
||||
grep -qx 276 < <(membership_references <<<"$membership_body")
|
||||
# The record ends at the next heading, so the progress view beside it is not
|
||||
# membership — that separation is the whole reason the two lists are distinct.
|
||||
check "a task-list reference is not a member" 1 "" \
|
||||
grep -qx 281 < <(membership_references <<<"$membership_body")
|
||||
check "...nor a reference in narration outside the record" 1 "" \
|
||||
grep -qx 906 < <(membership_references <<<"$membership_body")
|
||||
check "an unchecked row and a checked row enrol alike" 0 $'253\n264' \
|
||||
membership_references <<<"$membership_body"
|
||||
check "a bare row with no checkbox enrols too" 0 "257" \
|
||||
membership_references <<<"$membership_body"
|
||||
# Case-insensitive, trailing whitespace tolerated — the shape `## Task list`
|
||||
# already has, stated once in RELEASES.md and implemented once here.
|
||||
check "the heading matches case-insensitively with trailing whitespace" 0 "412" \
|
||||
membership_references <<<"$(printf '%s\n' '## MEMBERS ' '- #412 — admitted')"
|
||||
# Every CommonMark list marker opens a row, and only those. A row is whatever a
|
||||
# reader sees as one, so a marker class narrower than the set Markdown renders
|
||||
# would drop a member a human wrote — and take the standing window down with
|
||||
# it, which is silence in the one place D2 does not want silence. A class WIDER
|
||||
# than it is the same error mirrored, and the more dangerous direction: a line
|
||||
# no renderer reads as a row becomes a member, and one phantom open member
|
||||
# keeps a window standing and suppresses its non-member flag. Only the first
|
||||
# token rule decides what a row MEANS; the marker class decides what a row IS.
|
||||
marker_body="$(printf '%s\n' \
|
||||
'## Members' \
|
||||
'- #412 — a hyphen row' \
|
||||
'* #413 — an asterisk row' \
|
||||
'+ #414 — a plus row' \
|
||||
'1. #415 — an ordered row' \
|
||||
'2) #416 — an ordered row, the paren form' \
|
||||
'123456789. #419 — nine digits, the widest ordered marker there is' \
|
||||
'1234567890. #420 — ten digits, which CommonMark does not render as a row' \
|
||||
' #417 — no marker at all, so not a row')"
|
||||
check "every Markdown list marker opens a member row" 0 \
|
||||
"[412 413 414 415 416 419]" \
|
||||
membership_set <<<"$marker_body"
|
||||
check "a plus row enrols its member" 0 "" \
|
||||
grep -qx 414 < <(membership_references <<<"$marker_body")
|
||||
check "an ordered row enrols its member" 0 "" \
|
||||
grep -qx 415 < <(membership_references <<<"$marker_body")
|
||||
check "...and so does its paren form" 0 "" \
|
||||
grep -qx 416 < <(membership_references <<<"$marker_body")
|
||||
# The bound is CommonMark 5.2's: an ordered marker is 1 to 9 digits then `.` or
|
||||
# `)`. Both sides of it are asserted, because one alone is met by a class that
|
||||
# is merely different rather than right — the 9-digit row is the widest marker
|
||||
# a renderer accepts and must enrol, the 10-digit line is not a list row at all
|
||||
# and must contribute nothing. The bound is written twice in the parser, in the
|
||||
# row match and in the marker strip; a widening of either reds the pair.
|
||||
check "the widest ordered marker CommonMark allows enrols its member" 0 "" \
|
||||
grep -qx 419 < <(membership_references <<<"$marker_body")
|
||||
check "a tenth digit is not an ordered marker, so the line is not a row" 1 "" \
|
||||
grep -qx 420 < <(membership_references <<<"$marker_body")
|
||||
# The marker is what makes the line a row, so a bare reference on its own line
|
||||
# is narration inside the record, not a member. Indented deliberately: at
|
||||
# column 0 a `#` would end the record as a heading, and this assertion is about
|
||||
# the marker, not the terminator.
|
||||
check "a line with no list marker is not a row" 1 "" \
|
||||
grep -qx 417 < <(membership_references <<<"$marker_body")
|
||||
# Indentation bounds the row the way the digit count bounds the marker, and both
|
||||
# sides are asserted for the same reason: a bound met on one side alone is a
|
||||
# class merely different rather than right. Three spaces still open a row —
|
||||
# CommonMark 4.4 allows up to three, and refusing them would drop a member a
|
||||
# human wrote and reads. A fourth does not, and what it means depends on context
|
||||
# the line itself does not carry: GitHub renders ` - #N` after `## Members`
|
||||
# as `<pre><code>`, and the same bytes under a `- #N` row as a nested `<li>`.
|
||||
# The record is FLAT, so both are silence: an indented code block is not a row at
|
||||
# all, and a sub-bullet annotating a member row is not a second member. Enrolling
|
||||
# either is the phantom-member direction — an open reference taken from non-row
|
||||
# content keeps a window standing and suppresses its non-member flag. A leading
|
||||
# tab is four columns wherever indentation decides block structure, so it falls
|
||||
# under the same bound.
|
||||
indent_body="$(printf '%s\n' \
|
||||
'## Members' \
|
||||
'- #421 — column zero' \
|
||||
' - #422 — three spaces, the deepest indentation that still opens a row' \
|
||||
' - #423 — four spaces: a sub-row here, an indented code block alone' \
|
||||
$'\t- #424 — a tab, the same four columns, so neither is it')"
|
||||
check "the record admits exactly its unindented and shallowly indented rows" 0 \
|
||||
"[421 422]" \
|
||||
membership_set <<<"$indent_body"
|
||||
check "three spaces still open a row" 0 "" \
|
||||
grep -qx 422 < <(membership_references <<<"$indent_body")
|
||||
check "a row indented past the bound is a sub-row, and not a second member" 1 "" \
|
||||
grep -qx 423 < <(membership_references <<<"$indent_body")
|
||||
check "...and neither is the tab-indented one" 1 "" \
|
||||
grep -qx 424 < <(membership_references <<<"$indent_body")
|
||||
# The same bytes with no row above them, which is the shape the panel found: no
|
||||
# list is open, so the renderer reads an indented code block and there is nothing
|
||||
# for the record to enrol. A body whose record is entirely non-rows enumerates
|
||||
# no membership, and D4 then applies to it like any other empty record.
|
||||
code_block_body="$(printf '%s\n' \
|
||||
'## Members' \
|
||||
' - #425 — four spaces with no list open: an indented code block' \
|
||||
$'\t- #426 — and a tab, the same block')"
|
||||
check "an indented code block inside the record enrols nobody" 0 "[]" \
|
||||
membership_set <<<"$code_block_body"
|
||||
# The terminator itself, pinned where it can be seen: the record ends at the
|
||||
# next line starting with `#`, the shape `## Task list` already has. A bare
|
||||
# unindented reference is therefore the end of the record, not a member of it,
|
||||
# and the rows after it are outside.
|
||||
check "an unindented bare reference ends the record" 0 "[412]" \
|
||||
membership_set <<<"$(printf '%s\n' '## Members' '- #412' '#417' '- #418')"
|
||||
|
||||
# -- the carrier decision reads the record (#343 D3, D4, D5) ----------------
|
||||
check "an open member in the record makes the release issue a carrier" 0 \
|
||||
$'249\t253\n249\t257' \
|
||||
release_window_members 249 $'249\n253' \
|
||||
<<<"$(printf '%s\n' '## Members' '- #253' '- #257')"
|
||||
# D4: no fallback. This is THE defect's own state — #317 from its mint until
|
||||
# #249 closed at 2026-08-05T11:12Z, and crew's fifteen version epics at 0.6.0
|
||||
# adoption: a version epic declaring its predecessor exactly as *Gates*
|
||||
# instructs and enumerating nothing. Falling back to the gate here restores
|
||||
# the reading that made a shut window stand.
|
||||
check "a declared open predecessor with no record is not a carrier (D4)" 1 "" \
|
||||
grep -q . < <(release_window_members 317 $'249\n317\n343' <<<'Blocked by #249.')
|
||||
check "...and an empty record is not a carrier either" 1 "" \
|
||||
grep -q . < <(release_window_members 249 $'249\n253' \
|
||||
<<<"$(printf '%s\n' '## Members' '' '## Task list' '- [ ] #253')")
|
||||
check "...nor is a record whose every member has closed" 1 "" \
|
||||
grep -q . < <(release_window_members 249 $'249\n264' \
|
||||
<<<"$(printf '%s\n' '## Members' '- #218' '- #230')")
|
||||
# D5: #327's self-exclusion, inherited rather than re-decided. Both readings
|
||||
# share release_window_records, so the guard cannot hold on one side only.
|
||||
check "a membership row naming the carrier contributes no member (D5)" 1 "" \
|
||||
grep -q . < <(release_window_members 249 $'249\n253' \
|
||||
<<<"$(printf '%s\n' '## Members' '- #249 — the sink itself')")
|
||||
# shellcheck disable=SC2016 # awk fields belong to awk, not the shell
|
||||
check "...and never contributes to WINDOW_MEMBERS beside a real member" 1 "" \
|
||||
awk -F '\t' '$2 == 249 { found = 1 } END { exit !found }' \
|
||||
< <(release_window_members 249 $'249\n253' \
|
||||
<<<"$(printf '%s\n' '## Members' '- #249' '- #253')")
|
||||
|
||||
# The snapshot may nominate a flag before this issue's own queue branch runs;
|
||||
# the pure second gate reads the state that branch actually concluded.
|
||||
check "a pass concluding ready still permits both board flags" 0 "" \
|
||||
board_flags_in_scope ready
|
||||
check "a pass concluding claimed still permits both board flags" 0 "" \
|
||||
board_flags_in_scope claimed
|
||||
check "a pass concluding post-merge silences both board flags" 1 "" \
|
||||
board_flags_in_scope post-merge
|
||||
|
||||
|
||||
# -- the window decision (#292 D1) ------------------------------------------
|
||||
window_board=$'249\tblocked,release\tRelease 0.6.0 — the board empties\n253\tclaimed\tissueflow-reconcile — a member\n264\tready\tTRIAGE.md — a non-member\n270\tepic\tsome epic — exempt\n271\tpost-merge\tsome item — exempt\n272\tblocked\tsome issue — already placed'
|
||||
check "a ready non-member is flagged during a standing window" 0 "264" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue