forked from heavy-duty/ceremony
feat: parse release membership records
This commit is contained in:
parent
d493c993b7
commit
b7a2b31f84
1 changed files with 107 additions and 5 deletions
|
|
@ -517,6 +517,14 @@ window_in_scope() { # $1 = comma-joined labels -> 0 subject to the window rule
|
||||||
unblocked_claimable "$1"
|
unblocked_claimable "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
board_flags_in_scope() { # $1 = queue state concluded by this issue's pass
|
||||||
|
# The board snapshot decides which issues might owe a flag, but this pass
|
||||||
|
# speaks only about the queue state it leaves behind (#327 D2). A derived
|
||||||
|
# claimed -> post-merge transition therefore cannot post the snapshot's
|
||||||
|
# now-false claim that the issue is still unblocked and claimable.
|
||||||
|
unblocked_claimable "$1"
|
||||||
|
}
|
||||||
|
|
||||||
collision_key_index() { # board records on stdin -> "key<TAB>number" in scope
|
collision_key_index() { # board records on stdin -> "key<TAB>number" in scope
|
||||||
local n labels title key
|
local n labels title key
|
||||||
while IFS=$'\t' read -r n labels title; do
|
while IFS=$'\t' read -r n labels title; do
|
||||||
|
|
@ -551,20 +559,114 @@ collision_flags() { # key index on stdin -> "number<TAB>key=carrier[,key=carrier
|
||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
window_flags() { # $1 gate members, $2 window carriers; records on stdin -> numbers
|
window_flags() { # $1 window members, $2 window carriers; records on stdin -> numbers
|
||||||
local n labels title gate="$1" carriers="$2"
|
local n labels title members="$1" carriers="$2"
|
||||||
[ -n "$carriers" ] || return 0
|
[ -n "$carriers" ] || return 0
|
||||||
while IFS=$'\t' read -r n labels title; do
|
while IFS=$'\t' read -r n labels title; do
|
||||||
[ -n "$n" ] || continue
|
[ -n "$n" ] || continue
|
||||||
window_in_scope "$labels" || continue
|
window_in_scope "$labels" || continue
|
||||||
grep -qxF "$n" <<<"$gate" && continue
|
grep -qxF "$n" <<<"$members" && continue
|
||||||
# The release issue is the graph's SINK, never one of its own members
|
# The carrier is the graph's SINK, so the flag excludes it explicitly;
|
||||||
# (#292 D2), so it can never be its own non-member.
|
# membership parsing is a separate decision and cannot prove this guard.
|
||||||
grep -qxF "$n" <<<"$carriers" && continue
|
grep -qxF "$n" <<<"$carriers" && continue
|
||||||
printf '%s\n' "$n"
|
printf '%s\n' "$n"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
membership_references() { # release body on stdin -> its enumerated members
|
||||||
|
# The membership record (#343 D2), read by HEADING and never by a marker
|
||||||
|
# phrase. `blocked_reference_records` unions every occurrence of its marker
|
||||||
|
# and runs each clause to its own sentence terminator — deliberate, and the
|
||||||
|
# right error direction for a `blocked` issue, but the wrong one here: a
|
||||||
|
# release body is mostly narration ABOUT its members, so a phrase parser
|
||||||
|
# takes references out of the prose. That is the mechanism that put
|
||||||
|
# heavy-duty/crew's `0.2.0` epic inside its own gate. The heading match is
|
||||||
|
# anchored for the same reason: crew#346 carries a literal
|
||||||
|
# `## The members, in claim order` heading, which a substring match reads as
|
||||||
|
# the record and an anchored one does not.
|
||||||
|
#
|
||||||
|
# One member per row, and it is the row's FIRST token after the list marker
|
||||||
|
# and an optional checkbox. `epic_references` prints the whole row and takes
|
||||||
|
# every local reference in it, which is right for a progress view and wrong
|
||||||
|
# here — measured on crew#346, whose member rows carry merged PR numbers,
|
||||||
|
# another repository's issues, and one issue annotated in its own row as
|
||||||
|
# explicitly NOT a member of the window. A first token that is not a bare
|
||||||
|
# local `#<number>` contributes nothing: silence, not a guess. A qualified
|
||||||
|
# reference is never a member either, because a window is one repository's
|
||||||
|
# DAG decided against one board read.
|
||||||
|
#
|
||||||
|
# A row is any Markdown list row, so the marker class is the whole CommonMark
|
||||||
|
# set and exactly it — `-`, `*`, `+`, and 1 to 9 digits then `.` or `)`
|
||||||
|
# (CommonMark 5.2). Recognising only some of them would drop a row a human
|
||||||
|
# wrote, and reads, as a member: silence is the correct answer to a row whose
|
||||||
|
# first token is not a bare local reference, and the wrong one to a member
|
||||||
|
# enumerated under a marker this parse did not know. Recognising MORE than
|
||||||
|
# them is the same error mirrored: `1234567890. #412` is not a list row to
|
||||||
|
# any renderer, so reading it as one takes a member out of narration, and one
|
||||||
|
# phantom open member keeps a window standing and suppresses its non-member
|
||||||
|
# flag. The bound is written twice, in the row match and in the strip, and
|
||||||
|
# both are pinned. `epic_references` matches a narrower class; it is a
|
||||||
|
# progress view with its own fixtures and is byte-unchanged here (#343 D7).
|
||||||
|
#
|
||||||
|
# Indentation is bounded the same way and for the same reason: at most three
|
||||||
|
# spaces open a row (CommonMark 4.4), and a leading tab is four columns of it
|
||||||
|
# wherever indentation decides block structure. Past that bound the line is
|
||||||
|
# not a top-level row, and which non-row it is depends on context this parse
|
||||||
|
# does not carry — GitHub renders ` - #412` 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, a sub-bullet annotating a member row is not a second member, and
|
||||||
|
# enrolling either is the tenth digit's phantom-member direction one axis
|
||||||
|
# over. Below the bound the answer goes the other way for the same reason: one
|
||||||
|
# to three spaces is byte-identical to a top-level row a human indented, so it
|
||||||
|
# enrols, and the sub-row that shape can also be is the price (#348).
|
||||||
|
awk '
|
||||||
|
tolower($0) ~ /^##[[:space:]]+members[[:space:]]*$/ { in_record = 1; next }
|
||||||
|
in_record && /^#/ { exit }
|
||||||
|
in_record && /^ {0,3}([-*+]|[0-9]{1,9}[.)])[[:space:]]+/ {
|
||||||
|
row = $0
|
||||||
|
sub(/^ {0,3}([-*+]|[0-9]{1,9}[.)])[[:space:]]+/, "", row)
|
||||||
|
sub(/^\[[ xX]\][[:space:]]+/, "", row)
|
||||||
|
split(row, token, "[[:space:]]+")
|
||||||
|
if (token[1] ~ /^#[0-9]+$/) print substr(token[1], 2)
|
||||||
|
}
|
||||||
|
' | sort -nu
|
||||||
|
}
|
||||||
|
|
||||||
|
release_window_records() { # $1 carrier, $2 open numbers; refs on stdin -> carrier<TAB>member
|
||||||
|
# A carrier is never a member of its own window (#327 D1, which #343 D5
|
||||||
|
# inherits rather than re-decides). Remove it before deciding whether any
|
||||||
|
# open member makes the window stand, and before returning every non-self
|
||||||
|
# reference that contributes to WINDOW_MEMBERS. One function, so the two
|
||||||
|
# readings below can never drift apart on that guard.
|
||||||
|
local carrier="$1" open_numbers="$2" members member
|
||||||
|
members="$(awk -v carrier="$carrier" '$0 != carrier')"
|
||||||
|
[ -n "$members" ] || return 0
|
||||||
|
grep -qxF -f <(printf '%s\n' "$open_numbers") <<<"$members" || return 0
|
||||||
|
while IFS= read -r member; do
|
||||||
|
[ -n "$member" ] && printf '%s\t%s\n' "$carrier" "$member"
|
||||||
|
done <<<"$members"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_window_gate() { # $1 carrier, $2 open issue numbers; body on stdin -> carrier<TAB>member
|
||||||
|
# #327 D1's reading of a release issue's `Blocked by` set, kept whole and
|
||||||
|
# kept driven. The window stopped consuming it at #343 D3 — a release
|
||||||
|
# epic's declaration answers its predecessor gate and nothing else — so
|
||||||
|
# what this keeps standing is the self-exclusion guard's other half: the
|
||||||
|
# gate side and the membership side share release_window_records, and this
|
||||||
|
# is where a change to it that only the gate could see reds.
|
||||||
|
blocked_references | release_window_records "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_window_members() { # $1 carrier, $2 open issue numbers; body on stdin -> carrier<TAB>member
|
||||||
|
# What the carrier decision reads (#343 D3). No fallback to the gate when
|
||||||
|
# the record is absent (#343 D4): a release issue enumerating no membership
|
||||||
|
# is not a carrier, and the board draws no window flag. A fallback would
|
||||||
|
# reinstate the misreading for precisely the bodies that have not been
|
||||||
|
# migrated, which is where it does its damage.
|
||||||
|
membership_references | release_window_records "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
window_state() { # $1 = window carriers -> the rendered state, "#249" | "#249, #250"
|
window_state() { # $1 = window carriers -> the rendered state, "#249" | "#249, #250"
|
||||||
awk 'NF { printf "%s#%s", (shown++ ? ", " : ""), $1 } END { printf "\n" }' <<<"$1"
|
awk 'NF { printf "%s#%s", (shown++ ? ", " : ""), $1 } END { printf "\n" }' <<<"$1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue