From 919135db12c8964c061e605291702396b9bc2e19 Mon Sep 17 00:00:00 2001 From: cndgrr <59120057+cndgrr@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:54:52 +0000 Subject: [PATCH] the two flags mean one thing by unblocked, and a key set is a set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B1: deliverable_keys answered a multiset, so a `+` title whose segments normalize to one key made collision_flags find the issue adjacent to itself and chain it to its own number — the comment asked an issue to declare `Blocked by` itself, and two such carriers corrupted the chain between them. The keys are deduped where the set property belongs. B2: window_in_scope excluded only blocked/epic/post-merge, admitting `needs-triage` and label-less issues, so the sweep could add `needs-triage` to an issue and then tell it about a mint-time membership call in the same pass. Both flags now call one unblocked_claimable predicate — #293 D2 corrected gives one gloss on `unblocked` and D3b says D3 uses it. The window log line says "unblocked", not "ready": D3b corrected exactly that wording, and the flag fires on `claimed` too. --- .../issueflow-reconcile.sh | 60 ++++++++++++++----- test/issueflow-reconcile.test.sh | 4 +- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index 94d2e64..41532c2 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -469,28 +469,56 @@ deliverable_keys() { # title on stdin -> its deliverable keys, one per line # segment: `TRIAGE.md + RELEASES.md` carries both keys. local segments=() IFS='+' read -r -a segments <<<"$prefix" - for segment in "${segments[@]}"; do - key="$(deliverable_key "$segment")" - [ -z "$key" ] || printf '%s\n' "$key" - done + # An issue answers a SET of keys, never a multiset. Normalization is + # many-to-one by design — `issueflow-reconcile.sh + issueflow-reconcile.test.sh` + # is one deliverable spelled twice, which is exactly the `+` shape D2 wrote + # the segment rule for — and a repeated key makes `collision_flags`' scan + # find the issue adjacent to itself, chaining it to its own number: the + # comment would ask #402 to declare `Blocked by #402`. It corrupts the chain + # between two such issues too, since each contributes two rows to one key. + # Deduping here rather than in the index keeps the set property with the + # function whose contract it is. + { for segment in "${segments[@]}"; do + key="$(deliverable_key "$segment")" + [ -z "$key" ] || printf '%s\n' "$key" + done + } | awk '!seen[$0]++' } -collision_in_scope() { # $1 = comma-joined labels -> 0 in the collision set +unblocked_claimable() { # $1 = comma-joined labels -> 0 when the issue is unblocked + # THE one definition of `unblocked`, because #293 gives both flags one word + # and one gloss on it: D2 as corrected reads "`unblocked` means open and not + # `blocked` — carrying `ready` or `claimed`, with or without an open PR", + # and D3b's first line says D3 uses D2's corrected `unblocked` and names the + # domain as the claimable set. Two spellings of one spec word is how the + # flags came to disagree about `needs-triage`, so there is one predicate and + # both flags call it. + # # `blocked` is out: a chained issue is the GOAL state of #288's rule, and - # flagging it would report the fix as the defect. `epic` and `post-merge` - # are out by #288 D6 — neither is picked by a builder — and they carry no - # queue label to admit them here anyway. + # flagging it would report the fix as the defect. Anything else without + # `ready` or `claimed` is out because it is not claimable — `needs-triage` + # and a label-less issue are not states a builder can pick up, and an + # unlabeled one is getting `needs-triage` from this very pass. `epic` and + # `post-merge` are out by #288 D6 and #292 D1 alike — neither is picked by a + # builder — and they carry no queue label to admit them here anyway. case ",$1," in *,blocked,*) return 1 ;; esac case ",$1," in *,ready,*|*,claimed,*) return 0 ;; esac return 1 } +collision_in_scope() { # $1 = comma-joined labels -> 0 in the collision set + unblocked_claimable "$1" +} + window_in_scope() { # $1 = comma-joined labels -> 0 subject to the window rule - # `epic` and `post-merge` are outside the claimable set and exempt by name - # (#292 D1); `blocked` is already placed behind something and is what the - # non-member leg of the mint-time call writes. - case ",$1," in *,blocked,*|*,epic,*|*,post-merge,*) return 1 ;; esac - return 0 + # The same `unblocked`, not a second reading of it. Excluding only + # `blocked`/`epic`/`post-merge` here admitted `needs-triage` and a + # label-less issue, which left the sweep adding `needs-triage` to an + # unlabeled issue and then, in the same pass, telling it about a membership + # call made at mint time. Neither is claimable; #292's invariant is stated + # over the claimable set (D3b), and its exemptions say why — `epic` and + # `post-merge` are exempt *because neither is claimable*. + unblocked_claimable "$1" } collision_key_index() { # board records on stdin -> "keynumber" in scope @@ -760,7 +788,11 @@ same parse every \`blocked\` issue is gated on, echoed on that issue. *Comment only: nothing on this path writes a label or changes a state. The marker carries the window itself, so an unchanged one never re-posts.*" >/dev/null - log "#$n: window flag — a ready non-member under $state" + # "unblocked", not "ready": the flag fires on `claimed` too, PR in + # flight or not, which is the one wording #293 D3b went out of its way + # to correct. The log line is read by a human deciding whether the + # sweep understood the board, so it says what the predicate says. + log "#$n: window flag — an unblocked non-member under $state" fi fi } diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 032e02d..be5b6de 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -2218,8 +2218,8 @@ check "the morning board draws exactly three collision flags" 0 "3" \ flag_count collision "$morning_out" # D3's corpus: the six `ready` non-members that raced the emptying gate. for nonmember in 257 264 266 276 281 284; do - check "#$nonmember is flagged as a ready non-member under #249" 0 \ - "issueflow: #$nonmember: window flag — a ready non-member under #249" \ + check "#$nonmember is flagged as an unblocked non-member under #249" 0 \ + "issueflow: #$nonmember: window flag — an unblocked non-member under #249" \ printf '%s\n' "$morning_out" done check "the morning board draws exactly six window flags" 0 "6" \