forked from heavy-duty/ceremony
fix: the issue-side ruling clock reads comments only (#284)
Claiming a needs-ruling issue dated it through the assigned timeline event, silencing the 7-day escalation nudge at exactly the moment a builder started working through it. The ruling block now reads last_issue_comment_activity (D1); the reclaim clock keeps the assignment (D2) because there the assignment IS the claim; post-merge hands its evidence read to the ruling block instead of reading again (D6, D7); the claimed branch reads both clocks at its top, before anything it posts. LABELS.md and lib/ruling.sh now say what each surface's clock reads (D4, D5). The #257-era order compositions move to the comments read — the timeline is no longer an input the issue clocks take, and its unreadability no longer holds unrelated writes hostage; that narrowing is pinned rather than implied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe45180616
commit
48547d5eb1
5 changed files with 263 additions and 47 deletions
|
|
@ -154,8 +154,11 @@ label never removed — and a ruling with no real activity for 7 days draws a
|
||||||
comment-only nudge addressed to the decider, linking the escalation. The
|
comment-only nudge addressed to the decider, linking the escalation. The
|
||||||
nudge carries no marker on purpose: the comment is itself activity, so it
|
nudge carries no marker on purpose: the comment is itself activity, so it
|
||||||
resets its own window and never repeats within a quiet week. Label churn is
|
resets its own window and never repeats within a quiet week. Label churn is
|
||||||
not activity — the clock reads comments, reviews and commits, or the sweep
|
never activity, or the sweep would reset itself — and each surface's clock
|
||||||
would reset itself.
|
reads what exists on it: on a pull request, comments, reviews and commits;
|
||||||
|
on an issue, comments alone. An assignment is the claim clock's fact, not
|
||||||
|
the ruling's — claiming a flagged issue does not answer it, and buys the
|
||||||
|
escalation no quiet (#284).
|
||||||
|
|
||||||
`offsite` is issue-only and records that a claimed issue's deliverable lives
|
`offsite` is issue-only and records that a claimed issue's deliverable lives
|
||||||
in another repository, where a closing reference cannot make a local open PR
|
in another repository, where a closing reference cannot make a local open PR
|
||||||
|
|
|
||||||
|
|
@ -515,19 +515,26 @@ issue_activity_at() { # $1 issue, $2 created_at, $3 with-assignment|comments-onl
|
||||||
}
|
}
|
||||||
|
|
||||||
last_issue_activity() { # $1 issue, $2 created_at → epoch; non-zero if a read failed
|
last_issue_activity() { # $1 issue, $2 created_at → epoch; non-zero if a read failed
|
||||||
# The claim clock, and the ruling clock with it. Assignment is the claim
|
# The claim clock, and only the claim clock (#284). Assignment is the claim
|
||||||
# itself. Ignoring it would let an old issue be reclaimed in the seconds
|
# itself. Ignoring it would let an old issue be reclaimed in the seconds
|
||||||
# between assignment and its required draft PR.
|
# between assignment and its required draft PR. The ruling clock no longer
|
||||||
|
# rides here: an assignment says nothing about whether the decider
|
||||||
|
# answered, and counting it let claiming a flagged issue buy its
|
||||||
|
# escalation another 7 quiet days.
|
||||||
issue_activity_at "$1" "$2" with-assignment
|
issue_activity_at "$1" "$2" with-assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
last_issue_comment_activity() { # $1 issue, $2 created_at → epoch; non-zero on a failed read
|
last_issue_comment_activity() { # $1 issue, $2 created_at → epoch; non-zero on a failed read
|
||||||
# The evidence nudge's clock (#254). Same computation, one input fewer, and
|
# The evidence nudge's clock (#254), and the issue-side ruling clock with
|
||||||
# the input it drops is the one that would starve the criterion: on
|
# it (#284): on an issue, a comment is the only substantive activity
|
||||||
|
# toward a ruling. Same computation as the claim clock, one input fewer,
|
||||||
|
# and the input it drops is the one that would starve each criterion: on
|
||||||
# `post-merge` there is no claim for an assignment to protect, and an
|
# `post-merge` there is no claim for an assignment to protect, and an
|
||||||
# assignee there is the invalid composition the `post-merge-assigned` flag
|
# assignee there is the invalid composition the `post-merge-assigned` flag
|
||||||
# reports. Counting it would let a broken board buy the item another 7 days
|
# reports; under `needs-ruling` the assignment is the *claim's* fact, and
|
||||||
# of silence — the failure direction of #254 taken backwards.
|
# counting it silenced the escalation at exactly the moment somebody
|
||||||
|
# started working through it. Either way, a wider clock would let board
|
||||||
|
# state buy the wait another 7 days of silence.
|
||||||
#
|
#
|
||||||
# A comment the sweep itself wrote is still activity here, deliberately:
|
# A comment the sweep itself wrote is still activity here, deliberately:
|
||||||
# the nudge carries no marker, so its own comment is what rate-limits it,
|
# the nudge carries no marker, so its own comment is what rate-limits it,
|
||||||
|
|
@ -538,7 +545,7 @@ last_issue_comment_activity() { # $1 issue, $2 created_at → epoch; non-zero on
|
||||||
}
|
}
|
||||||
|
|
||||||
reconcile_issue() {
|
reconcile_issue() {
|
||||||
local n="$1" decision refs cross_refs states age evidence_age created assignees open_pr=false label owners
|
local n="$1" decision refs cross_refs states age evidence_age ruling_age created assignees open_pr=false label owners
|
||||||
local merged_ref_pr="" transition_marker="" transition_handled=false parsed_set="" parse_marker=""
|
local merged_ref_pr="" transition_marker="" transition_handled=false parsed_set="" parse_marker=""
|
||||||
local unchecked="" remove_claimed=claimed
|
local unchecked="" remove_claimed=claimed
|
||||||
local attention_active=true attention_suppression=""
|
local attention_active=true attention_suppression=""
|
||||||
|
|
@ -557,6 +564,19 @@ reconcile_issue() {
|
||||||
if has_issue_label claimed; then
|
if has_issue_label claimed; then
|
||||||
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
|
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
|
||||||
grep -qxF "$n" <<<"${OPEN_PR_ISSUES:-}" && open_pr=true
|
grep -qxF "$n" <<<"${OPEN_PR_ISSUES:-}" && open_pr=true
|
||||||
|
# Under a pending ruling, the ruling clock is read at the top of the
|
||||||
|
# branch, before anything either arm below can post — the derived
|
||||||
|
# transition comment, the reclaim notice and the claimed-unassigned flag
|
||||||
|
# are all comments, and a read taken after one would date the issue by
|
||||||
|
# this sweep's own writing (#284; the hazard #274 met from the other
|
||||||
|
# side). Two clocks on purpose: `age` below counts the assignment
|
||||||
|
# because the assignment IS the claim; the ruling waits on a human, and
|
||||||
|
# an assignment says nothing about whether the decider answered.
|
||||||
|
if has_issue_label needs-ruling; then
|
||||||
|
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||||
|
guarded_read ruling_age last_issue_comment_activity "$n" "$created" \
|
||||||
|
|| skip_issue "$n" "could not read its activity history: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
||||||
|
fi
|
||||||
merged_ref_pr="$(post_merge_pr_for_issue "$n")"
|
merged_ref_pr="$(post_merge_pr_for_issue "$n")"
|
||||||
if [ -n "$merged_ref_pr" ]; then
|
if [ -n "$merged_ref_pr" ]; then
|
||||||
transition_marker="$(post_merge_transition_marker "$merged_ref_pr")"
|
transition_marker="$(post_merge_transition_marker "$merged_ref_pr")"
|
||||||
|
|
@ -636,28 +656,18 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
||||||
# The evidence nudge's clock is read BEFORE any comment this branch
|
# The evidence nudge's clock is read BEFORE any comment this branch
|
||||||
# posts. `ensure_comment` below is itself activity, so reading after it
|
# posts. `ensure_comment` below is itself activity, so reading after it
|
||||||
# would let the assigned-flag comment silence the nudge for another 7
|
# would let the assigned-flag comment silence the nudge for another 7
|
||||||
# days — the same self-silencing the ruling nudge avoids by reading its
|
# days — the same self-silencing the ruling nudge avoids by taking its
|
||||||
# facts once, at the top of the pass.
|
# clock from this same read, below.
|
||||||
#
|
|
||||||
# Its own variable, not `age`: the ruling block below reuses `age` when
|
|
||||||
# it is already set, and the evidence clock is deliberately narrower than
|
|
||||||
# the ruling clock. Leaking it there would silently change what a ruling
|
|
||||||
# nudge means depending on which queue label the issue sits under.
|
|
||||||
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||||
guarded_read evidence_age last_issue_comment_activity "$n" "$created" \
|
guarded_read evidence_age last_issue_comment_activity "$n" "$created" \
|
||||||
|| skip_issue "$n" "could not read its activity history: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
|| skip_issue "$n" "could not read its activity history: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
||||||
# The ruling clock is read HERE, not in the ruling block, for the same
|
# On this surface the ruling clock IS this read (#284 D6): both nudges
|
||||||
# reason the evidence clock is: that block reads only when `age` is
|
# wait on comments and nothing else, so the evidence clock is handed to
|
||||||
# unset, and by the time it runs this branch may have posted the
|
# the ruling block rather than read again — and handed HERE, before the
|
||||||
# evidence nudge — so its read would date the issue by this sweep's own
|
# assigned-flag comment and the evidence nudge below, so neither wait is
|
||||||
# comment and silence the ruling nudge. Both waits are answered from
|
# ever answered by anything this pass writes. `post-merge` +
|
||||||
# facts that predate anything this pass writes. The cost is one extra
|
# `needs-ruling` now costs one comments read where it cost three.
|
||||||
# comments read on `post-merge` + `needs-ruling`, and only there: an
|
ruling_age="$evidence_age"
|
||||||
# ordinary `post-merge` issue reads once.
|
|
||||||
if has_issue_label needs-ruling; then
|
|
||||||
guarded_read age last_issue_activity "$n" "$created" \
|
|
||||||
|| skip_issue "$n" "could not read its activity history: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
|
||||||
fi
|
|
||||||
if [ "$assignees" -gt 0 ] || has_issue_label attention; then
|
if [ "$assignees" -gt 0 ] || has_issue_label attention; then
|
||||||
ensure_comment "$n" post-merge-assigned \
|
ensure_comment "$n" post-merge-assigned \
|
||||||
'This `post-merge` issue has an assignee or `attention`. The sweep will not undo hand-set intent; triage must clear the invalid composition or move the issue back into buildable queue state.'
|
'This `post-merge` issue has an assignee or `attention`. The sweep will not undo hand-set intent; triage must clear the invalid composition or move the issue back into buildable queue state.'
|
||||||
|
|
@ -796,12 +806,19 @@ See \`$release_doctrine_path\`. The operator blessing the order is the one step
|
||||||
run gh issue edit "$n" -R "$REPO" --remove-label stale >/dev/null
|
run gh issue edit "$n" -R "$REPO" --remove-label stale >/dev/null
|
||||||
log "#$n: unstale (a ruling is pending)"
|
log "#$n: unstale (a ruling is pending)"
|
||||||
fi
|
fi
|
||||||
if [ -z "${age:-}" ]; then
|
# The ruling clock reads comments only (#284 D1): an `assigned` event is
|
||||||
|
# the claim clock's fact, and counting it here let claiming a flagged
|
||||||
|
# issue buy its escalation another 7 quiet days. The reclaim clock
|
||||||
|
# (`age`) must never reach this call — the branches that write comments
|
||||||
|
# before this block (`claimed`, `post-merge`) arrive holding
|
||||||
|
# `ruling_age` already, read before anything they post; the fresh read
|
||||||
|
# serves the paths that arrive empty-handed.
|
||||||
|
if [ -z "${ruling_age:-}" ]; then
|
||||||
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||||
guarded_read age last_issue_activity "$n" "$created" \
|
guarded_read ruling_age last_issue_comment_activity "$n" "$created" \
|
||||||
|| skip_issue "$n" "could not read its activity history: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
|| skip_issue "$n" "could not read its activity history: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
||||||
fi
|
fi
|
||||||
reconcile_ruling "$n" "$age" "$NOW"
|
reconcile_ruling "$n" "$ruling_age" "$NOW"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
6
changelog.d/284.md
Normal file
6
changelog.d/284.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Claiming a `needs-ruling` issue no longer buys its escalation another 7
|
||||||
|
quiet days: the issue-side ruling clock reads comments alone — an
|
||||||
|
assignment is the claim clock's fact — and LABELS.md now names what each
|
||||||
|
surface's clock reads (#284).
|
||||||
|
|
@ -175,8 +175,11 @@ ruling_default_decision() { # escalation body on stdin → DEADLINE <ts> | HARDB
|
||||||
}
|
}
|
||||||
|
|
||||||
ruling_nudge_decision() { # $1 now, $2 last real-activity epoch → NUDGE | KEEP
|
ruling_nudge_decision() { # $1 now, $2 last real-activity epoch → NUDGE | KEEP
|
||||||
# Real activity only — comments, reviews, commits, never label churn, or
|
# Real activity only, as the caller's surface defines it: the PR sweep
|
||||||
# the sweep would reset its own clock. The nudge needs NO marker: the
|
# supplies comments, reviews and commits; the issue sweeps supply comments
|
||||||
|
# alone — an `assigned` event is the claim clock's fact, and counting it
|
||||||
|
# let a claim silence a pending ruling (#284). Never label churn, or the
|
||||||
|
# sweep would reset its own clock. The nudge needs NO marker: the
|
||||||
# nudge comment is itself activity, so posting it resets this window and
|
# nudge comment is itself activity, so posting it resets this window and
|
||||||
# the rule self-rate-limits to at most one nudge per 7 quiet days. That is
|
# the rule self-rate-limits to at most one nudge per 7 quiet days. That is
|
||||||
# deliberate — a later refactor that "fixes" it by adding a marker breaks
|
# deliberate — a later refactor that "fixes" it by adding a marker breaks
|
||||||
|
|
|
||||||
|
|
@ -900,6 +900,167 @@ check "...it is reused from lib/ruling.sh" 0 "" \
|
||||||
check "the evidence-nudge probes perform no issue edits" 0 "$nudge_edits_before" \
|
check "the evidence-nudge probes perform no issue edits" 0 "$nudge_edits_before" \
|
||||||
bash -c 'wc -l <"$1"' _ "$TMP/issue-edits"
|
bash -c 'wc -l <"$1"' _ "$TMP/issue-edits"
|
||||||
|
|
||||||
|
# -- the issue-side ruling clock is comments-only (#284) ---------------------
|
||||||
|
# #52 D10's "reuse the activity computation" made the issue-side ruling nudge
|
||||||
|
# ride the claim-reclamation clock, `assigned` events included — so claiming
|
||||||
|
# a flagged issue dated it, and the escalation the flag exists to keep
|
||||||
|
# visible went quiet for another 7 days at exactly the moment somebody
|
||||||
|
# started working through it. The ruling clock is now the comments-only one
|
||||||
|
# (D1); the reclaim clock keeps the assignment (D2), because there the
|
||||||
|
# assignment IS the claim. Every must-nudge probe below was run against the
|
||||||
|
# pre-#284 sweep and went red — the #274 round-1 discipline: the fixture
|
||||||
|
# proves the defect, not merely the fix.
|
||||||
|
ruling_clock_edits_before="$(wc -l <"$TMP/issue-edits")"
|
||||||
|
ruling_quiet() { # $1 issue — conforming escalation, both rungs fired, ~9d of comment quiet
|
||||||
|
jq -n --arg l "$(iso_at $((INOW - 10 * 86400)))" \
|
||||||
|
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l}]' \
|
||||||
|
>"$(tfix "$1")"
|
||||||
|
jq -n --arg at "$(iso_at $((INOW - 10 * 86400 - 60)))" \
|
||||||
|
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: none — hard block' \
|
||||||
|
--arg r12 "$(iso_at $((INOW - 10 * 86400 + 13 * 3600)))" \
|
||||||
|
--arg r24 "$(iso_at $((INOW - 10 * 86400 + 25 * 3600)))" \
|
||||||
|
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc","body":$b},
|
||||||
|
{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nrung"},
|
||||||
|
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nrung"}]' \
|
||||||
|
>"$(cfix "$1")"
|
||||||
|
}
|
||||||
|
timeline_add() { # $1 issue, $2 event, $3 seconds ago
|
||||||
|
jq --arg e "$2" --arg at "$(iso_at $((INOW - $3)))" \
|
||||||
|
'. + [{"event":$e,"created_at":$at}]' \
|
||||||
|
"$(tfix "$1")" >"$(tfix "$1").tmp" && mv "$(tfix "$1").tmp" "$(tfix "$1")"
|
||||||
|
}
|
||||||
|
comment_add() { # $1 issue, $2 seconds ago
|
||||||
|
jq --arg at "$(iso_at $((INOW - $2)))" \
|
||||||
|
'. + [{"user":{"login":"decider"},"created_at":$at,"html_url":"https://x/d","body":"still thinking"}]' \
|
||||||
|
"$(cfix "$1")" >"$(cfix "$1").tmp" && mv "$(cfix "$1").tmp" "$(cfix "$1")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The live shape, and the defect: a builder claims the flagged issue, the
|
||||||
|
# assignment is an hour old, the decider has been silent ~9 days.
|
||||||
|
ruling_quiet 101
|
||||||
|
timeline_add 101 assigned 3600
|
||||||
|
claimed_fresh="$(issue_probe 101 $'claimed\nneeds-ruling' 1 true)"
|
||||||
|
check "an hour-old claim does not silence a ruling 9 days quiet" 0 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$claimed_fresh"
|
||||||
|
check "...and the fresh assignment is not reclaim bait either" 1 "" \
|
||||||
|
grep -q 'reclaimed' <<<"$claimed_fresh"
|
||||||
|
|
||||||
|
# The clock rule alone, no assignee in the way: an assigned/unassigned pair
|
||||||
|
# in the timeline is the claim's history, not activity toward the ruling.
|
||||||
|
ruling_quiet 102
|
||||||
|
timeline_add 102 assigned 3600
|
||||||
|
timeline_add 102 unassigned 3500
|
||||||
|
ready_pair="$(issue_probe 102 $'ready\nneeds-ruling' 0)"
|
||||||
|
check "a ready issue nudges through an hour-old assignment pair" 0 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$ready_pair"
|
||||||
|
|
||||||
|
# post-merge + needs-ruling fires BOTH nudges in one sweep, from one read
|
||||||
|
# taken before either write. This probe is also the read-order pin: the
|
||||||
|
# evidence nudge posts first and the stub stamps it as fresh activity, so
|
||||||
|
# restoring a ruling-clock read below `ensure_comment` turns the second
|
||||||
|
# check red — the hazard #274 met and killed inside one round.
|
||||||
|
ruling_quiet 103
|
||||||
|
timeline_add 103 assigned 3600
|
||||||
|
timeline_add 103 unassigned 3500
|
||||||
|
both_fresh="$(issue_probe 103 $'post-merge\nneeds-ruling' 0)"
|
||||||
|
check "a fresh assignment starves neither post-merge wait" 0 "" \
|
||||||
|
grep -q 'post-merge evidence nudge' <<<"$both_fresh"
|
||||||
|
check "...the ruling nudge fires beside it, not behind it" 0 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$both_fresh"
|
||||||
|
|
||||||
|
# blocked composes the same way. The #252 parse echo is pre-seeded old so
|
||||||
|
# the probe isolates the clock rule — steady state, where the echo for this
|
||||||
|
# parse set already exists and the branch posts nothing before the tail.
|
||||||
|
ruling_quiet 104
|
||||||
|
timeline_add 104 assigned 3600
|
||||||
|
refs_104="$(blocked_references <<<'Blocked by #999.')"
|
||||||
|
cross_104="$(blocked_cross_references <<<'Blocked by #999.')"
|
||||||
|
marker_104="$(blocked_parse_marker "$(blocked_parse_set "$refs_104" "$cross_104")")"
|
||||||
|
jq --arg m "$marker_104" --arg at "$(iso_at $((INOW - 9 * 86400)))" \
|
||||||
|
'. + [{"user":{"login":"sweep-bot"},"created_at":$at,"html_url":"https://x/echo","body":("<!-- issueflow:" + $m + " -->\necho")}]' \
|
||||||
|
"$(cfix 104)" >"$(cfix 104).tmp" && mv "$(cfix 104).tmp" "$(cfix 104)"
|
||||||
|
blocked_fresh="$(issue_probe 104 $'blocked\nneeds-ruling' 0 false "" 'Blocked by #999.')"
|
||||||
|
check "a blocked issue nudges through an hour-old assignment" 0 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$blocked_fresh"
|
||||||
|
|
||||||
|
# 6 days of comment quiet is 6, with or without an assignment inside it.
|
||||||
|
ruling_quiet 105
|
||||||
|
timeline_add 105 assigned 3600
|
||||||
|
comment_add 105 $((6 * 86400))
|
||||||
|
six_days="$(issue_probe 105 $'claimed\nneeds-ruling' 1 true)"
|
||||||
|
check "6 days of comment quiet draws no nudge" 1 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$six_days"
|
||||||
|
|
||||||
|
# Label churn is not activity on this clock either — it reads no timeline
|
||||||
|
# at all, which closes the class rather than the spelling.
|
||||||
|
ruling_quiet 106
|
||||||
|
timeline_add 106 labeled 1800
|
||||||
|
timeline_add 106 unlabeled 1700
|
||||||
|
churn="$(issue_probe 106 $'ready\nneeds-ruling' 0)"
|
||||||
|
check "hour-old label churn does not hold the nudge back" 0 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$churn"
|
||||||
|
|
||||||
|
# Self-rate-limiting, asserted as the property (#254's discipline): sweep
|
||||||
|
# again a day after probe 101's nudge and the nudge it posted is the
|
||||||
|
# activity that keeps it silent — no marker involved.
|
||||||
|
day_after="$(PROBE_NOW=$((INOW + 86400)) issue_probe 101 $'claimed\nneeds-ruling' 1 true)"
|
||||||
|
check "the sweep a day after its nudge holds its silence" 1 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$day_after"
|
||||||
|
|
||||||
|
# No flag, no nudge, whatever the clock says.
|
||||||
|
quiet_comment 107 $((60 * 86400))
|
||||||
|
noflag="$(issue_probe 107 ready 0)"
|
||||||
|
check "an unflagged issue draws no ruling nudge at any age" 1 "" \
|
||||||
|
grep -q 'ruling nudge' <<<"$noflag"
|
||||||
|
|
||||||
|
# D2's input doing its job — the one thing a "the clocks are the same now,
|
||||||
|
# merge them" refactor would break. Red the instant `last_issue_activity`
|
||||||
|
# loses `assigned`.
|
||||||
|
printf '[]\n' >"$(cfix 108)"
|
||||||
|
jq -n --arg at "$(iso_at $((INOW - 600)))" \
|
||||||
|
'[{"event":"assigned","created_at":$at}]' >"$(tfix 108)"
|
||||||
|
not_reclaimed="$(issue_probe 108 claimed 1 false)"
|
||||||
|
check "a 10-minute-old claim on a silent issue is not reclaimed" 1 "" \
|
||||||
|
grep -q 'reclaimed' <<<"$not_reclaimed"
|
||||||
|
|
||||||
|
# One fixture, two clocks, asserted directly and not by inspection: the
|
||||||
|
# newest event is the assignment; the reclaim clock returns it and the
|
||||||
|
# ruling clock returns the older comment.
|
||||||
|
jq -n --arg at "$(iso_at $((INOW - 8 * 86400)))" \
|
||||||
|
'[{"user":{"login":"decider"},"created_at":$at,"html_url":"https://x/c9","body":"x"}]' >"$(cfix 109)"
|
||||||
|
jq -n --arg at "$(iso_at $((INOW - 3600)))" \
|
||||||
|
'[{"event":"assigned","created_at":$at}]' >"$(tfix 109)"
|
||||||
|
clock_read() { # $1 clock fn — both against fixture 109
|
||||||
|
( REPO=owner/repo
|
||||||
|
# shellcheck disable=SC2317 # reached indirectly, through the clock under test
|
||||||
|
gh() { issue_stub_gh "$@"; }
|
||||||
|
"$1" 109 "$(iso_at $((INOW - 10 * 86400)))" )
|
||||||
|
}
|
||||||
|
check "one fixture, two clocks: the reclaim clock returns the assignment" \
|
||||||
|
0 "$((INOW - 3600))" clock_read last_issue_activity
|
||||||
|
check "...and the ruling clock returns the older comment" \
|
||||||
|
0 "$((INOW - 8 * 86400))" clock_read last_issue_comment_activity
|
||||||
|
|
||||||
|
# The mechanical call-site pins: the reclaim clock can never reach
|
||||||
|
# reconcile_ruling, on any path, asserted against the source and not the
|
||||||
|
# diff. Beside them, the one-spelling pins this issue inherits stay green.
|
||||||
|
# shellcheck disable=SC2016 # the call sites are asserted as literals
|
||||||
|
check "reconcile_ruling is never handed the reclaim clock" 1 "" \
|
||||||
|
grep -E '^[^#]*reconcile_ruling.*\$age' \
|
||||||
|
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
|
||||||
|
# shellcheck disable=SC2016 # the call site is asserted as a literal
|
||||||
|
check "...its one call site is fed the comments-only clock" 0 "1" \
|
||||||
|
grep -cF 'reconcile_ruling "$n" "$ruling_age" "$NOW"' \
|
||||||
|
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
|
||||||
|
# shellcheck disable=SC2016 # the guarded_read is asserted as a literal
|
||||||
|
check "...and ruling_age is never fed by the reclaim clock" 1 "" \
|
||||||
|
grep -E 'ruling_age.*last_issue_activity ' \
|
||||||
|
"$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2016 # positional parameter belongs to the isolated shell
|
||||||
|
check "the #284 probes perform no issue edits" 0 "$ruling_clock_edits_before" \
|
||||||
|
bash -c 'wc -l <"$1"' _ "$TMP/issue-edits"
|
||||||
|
|
||||||
# -- non-triggers stay byte-for-byte outside the transition ------------------
|
# -- non-triggers stay byte-for-byte outside the transition ------------------
|
||||||
recent_timeline() {
|
recent_timeline() {
|
||||||
jq -n --arg at "$(iso_at $((INOW - 60)))" \
|
jq -n --arg at "$(iso_at $((INOW - 60)))" \
|
||||||
|
|
@ -1800,52 +1961,78 @@ check "...and crew#329's label is not written on the way out" 1 "" \
|
||||||
check "...and nothing claims it was" 1 "" \
|
check "...and nothing claims it was" 1 "" \
|
||||||
grep -qF '#81: needs-triage (no queue state)' <<<"$broken_mint"
|
grep -qF '#81: needs-triage (no queue state)' <<<"$broken_mint"
|
||||||
|
|
||||||
# -- 3. the blockers->ready flip, then a failed TIMELINE read ----------------
|
# -- 3. the blockers->ready flip, then a failed COMMENTS read ----------------
|
||||||
# The wider class: the failing read is the second one inside
|
# The read that guards this branch is the marker check ahead of the parse
|
||||||
# last_issue_activity, so the comments read answers and the marker check and
|
# echo: a broken comments endpoint skips there, before the echo or the flip
|
||||||
# the flip both complete first. A comment AND a label edit are staged.
|
# is staged. The timeline is no longer an input on this path at all (#284
|
||||||
|
# D1 — the ruling clock reads comments alone), so the unreadable-timeline
|
||||||
|
# case moved from "skips everything" to its own pin below.
|
||||||
printf '%s\n' '{"number":82,"state":"closed"}' \
|
printf '%s\n' '{"number":82,"state":"closed"}' \
|
||||||
>"$ORDER/repos_owner_repo_issues_82.json"
|
>"$ORDER/repos_owner_repo_issues_82.json"
|
||||||
order_fixture 83 '[{"name":"blocked"},{"name":"needs-ruling"}]' 'Blocked by #82.'
|
order_fixture 83 '[{"name":"blocked"},{"name":"needs-ruling"}]' 'Blocked by #82.'
|
||||||
order_board '[{"number":83}]'
|
order_board '[{"number":83}]'
|
||||||
|
order_heals 83 comments
|
||||||
order_heals 83 timeline
|
order_heals 83 timeline
|
||||||
healthy_flip="$(order_run)"
|
healthy_flip="$(order_run)"
|
||||||
check "the control: a healthy pass really does flip cleared blockers to ready" 0 \
|
check "the control: a healthy pass really does flip cleared blockers to ready" 0 \
|
||||||
"issueflow: #83: blockers closed -> ready" printf '%s\n' "$healthy_flip"
|
"issueflow: #83: blockers closed -> ready" printf '%s\n' "$healthy_flip"
|
||||||
check "...writing the label edit" 0 "" order_wrote 83 edit
|
check "...writing the label edit" 0 "" order_wrote 83 edit
|
||||||
check "...and posting the blockers-cleared comment" 0 "" order_wrote 83 comment
|
check "...and posting the blockers-cleared comment" 0 "" order_wrote 83 comment
|
||||||
order_breaks 83 timeline
|
order_breaks 83 comments
|
||||||
broken_flip="$(order_run)"
|
broken_flip="$(order_run)"
|
||||||
check "a failed timeline read skips the blockers->ready composition" 0 \
|
check "a failed comments read skips the blockers->ready composition" 0 \
|
||||||
"issueflow: #83: skipped this pass — could not read its activity history: $GH_STUB_STDERR" \
|
"issueflow: #83: skipped this pass — could not read its comments: $GH_STUB_STDERR" \
|
||||||
printf '%s\n' "$broken_flip"
|
printf '%s\n' "$broken_flip"
|
||||||
check "...leaving the issue blocked" 1 "" order_wrote 83 edit
|
check "...leaving the issue blocked" 1 "" order_wrote 83 edit
|
||||||
check "...with no comment posted about it" 1 "" order_wrote 83 comment
|
check "...with no comment posted about it" 1 "" order_wrote 83 comment
|
||||||
check "...and nothing claiming the flip happened" 1 "" \
|
check "...and nothing claiming the flip happened" 1 "" \
|
||||||
grep -qF 'blockers closed -> ready' <<<"$broken_flip"
|
grep -qF 'blockers closed -> ready' <<<"$broken_flip"
|
||||||
|
# The read this path no longer takes cannot skip it (#284): with comments
|
||||||
|
# healthy and the timeline broken, the flip commits, and only the ruling
|
||||||
|
# ladder's own soft-failing read goes without — no verdict is invented, and
|
||||||
|
# no unrelated write is held hostage by an input the clocks stopped reading.
|
||||||
|
order_heals 83 comments
|
||||||
|
order_breaks 83 timeline
|
||||||
|
narrowed_flip="$(order_run)"
|
||||||
|
check "a failed timeline read no longer skips the flip" 1 "" \
|
||||||
|
grep -qF 'skipped this pass' <<<"$narrowed_flip"
|
||||||
|
check "...the flip commits" 0 "" order_wrote 83 edit
|
||||||
|
check "...and the ruling ladder says what it could not read" 0 \
|
||||||
|
"issueflow: #83: ruling timeline unreadable — no verdict invented this pass" \
|
||||||
|
printf '%s\n' "$narrowed_flip"
|
||||||
|
|
||||||
# -- 4. a posted nudge, then a failed TIMELINE read -------------------------
|
# -- 4. a posted nudge, then a failed COMMENTS read -------------------------
|
||||||
# The comment-only half of the class: an epic nudge is staged, and the
|
# The comment-only half of the class: the epic nudge's own marker check is
|
||||||
# ruling tail's activity read fails after it. A comment is as much a
|
# the read that fails, so the nudge is never staged and the skip reports the
|
||||||
# mutation as a label — it is the thing markers exist to make idempotent.
|
# truth. A comment is as much a mutation as a label — it is the thing
|
||||||
|
# markers exist to make idempotent.
|
||||||
order_fixture 84 '[{"name":"epic"},{"name":"needs-ruling"}]' \
|
order_fixture 84 '[{"name":"epic"},{"name":"needs-ruling"}]' \
|
||||||
'## Task list
|
'## Task list
|
||||||
|
|
||||||
- [x] #82'
|
- [x] #82'
|
||||||
order_board '[{"number":84}]'
|
order_board '[{"number":84}]'
|
||||||
|
order_heals 84 comments
|
||||||
order_heals 84 timeline
|
order_heals 84 timeline
|
||||||
healthy_nudge="$(order_run)"
|
healthy_nudge="$(order_run)"
|
||||||
check "the control: a healthy pass really does nudge a completed epic" 0 \
|
check "the control: a healthy pass really does nudge a completed epic" 0 \
|
||||||
"issueflow: #84: completed epic nudged" printf '%s\n' "$healthy_nudge"
|
"issueflow: #84: completed epic nudged" printf '%s\n' "$healthy_nudge"
|
||||||
check "...by posting a comment" 0 "" order_wrote 84 comment
|
check "...by posting a comment" 0 "" order_wrote 84 comment
|
||||||
order_breaks 84 timeline
|
order_breaks 84 comments
|
||||||
broken_nudge="$(order_run)"
|
broken_nudge="$(order_run)"
|
||||||
check "a failed timeline read skips the epic-nudge composition" 0 \
|
check "a failed comments read skips the epic-nudge composition" 0 \
|
||||||
"issueflow: #84: skipped this pass — could not read its activity history: $GH_STUB_STDERR" \
|
"issueflow: #84: skipped this pass — could not read its comments: $GH_STUB_STDERR" \
|
||||||
printf '%s\n' "$broken_nudge"
|
printf '%s\n' "$broken_nudge"
|
||||||
check "...and the nudge comment is never posted" 1 "" order_wrote 84 comment
|
check "...and the nudge comment is never posted" 1 "" order_wrote 84 comment
|
||||||
check "...and nothing claims it was" 1 "" \
|
check "...and nothing claims it was" 1 "" \
|
||||||
grep -qF 'completed epic nudged' <<<"$broken_nudge"
|
grep -qF 'completed epic nudged' <<<"$broken_nudge"
|
||||||
|
# The narrowed surface again (#284): a broken timeline neither skips nor
|
||||||
|
# suppresses the nudge; the ruling ladder alone goes without a verdict.
|
||||||
|
order_heals 84 comments
|
||||||
|
order_breaks 84 timeline
|
||||||
|
narrowed_nudge="$(order_run)"
|
||||||
|
check "a failed timeline read no longer skips the epic nudge" 1 "" \
|
||||||
|
grep -qF 'skipped this pass' <<<"$narrowed_nudge"
|
||||||
|
check "...the nudge commits" 0 "" order_wrote 84 comment
|
||||||
|
|
||||||
# -- the skip is still just a skip: counted, tailed, and green (D4, D6, D7) --
|
# -- the skip is still just a skip: counted, tailed, and green (D4, D6, D7) --
|
||||||
check "a mutation-bearing composition that skips is still not a crash" 1 "" \
|
check "a mutation-bearing composition that skips is still not a crash" 1 "" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue