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
|
||||
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
|
||||
not activity — the clock reads comments, reviews and commits, or the sweep
|
||||
would reset itself.
|
||||
never activity, or the sweep would reset itself — and each surface's clock
|
||||
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
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
}
|
||||
|
||||
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 input it drops is the one that would starve the criterion: on
|
||||
# The evidence nudge's clock (#254), and the issue-side ruling clock with
|
||||
# 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
|
||||
# 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
|
||||
# of silence — the failure direction of #254 taken backwards.
|
||||
# reports; under `needs-ruling` the assignment is the *claim's* fact, and
|
||||
# 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:
|
||||
# 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() {
|
||||
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 unchecked="" remove_claimed=claimed
|
||||
local attention_active=true attention_suppression=""
|
||||
|
|
@ -557,6 +564,19 @@ reconcile_issue() {
|
|||
if has_issue_label claimed; then
|
||||
assignees="$(jq '.assignees | length' <<<"$ISSUE_JSON")"
|
||||
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")"
|
||||
if [ -n "$merged_ref_pr" ]; then
|
||||
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
|
||||
# posts. `ensure_comment` below is itself activity, so reading after it
|
||||
# would let the assigned-flag comment silence the nudge for another 7
|
||||
# days — the same self-silencing the ruling nudge avoids by reading its
|
||||
# facts once, at the top of the pass.
|
||||
#
|
||||
# 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.
|
||||
# days — the same self-silencing the ruling nudge avoids by taking its
|
||||
# clock from this same read, below.
|
||||
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||
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")"
|
||||
# The ruling clock is read HERE, not in the ruling block, for the same
|
||||
# reason the evidence clock is: that block reads only when `age` is
|
||||
# unset, and by the time it runs this branch may have posted the
|
||||
# evidence nudge — so its read would date the issue by this sweep's own
|
||||
# comment and silence the ruling nudge. Both waits are answered from
|
||||
# facts that predate anything this pass writes. The cost is one extra
|
||||
# comments read on `post-merge` + `needs-ruling`, and only there: an
|
||||
# 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
|
||||
# On this surface the ruling clock IS this read (#284 D6): both nudges
|
||||
# wait on comments and nothing else, so the evidence clock is handed to
|
||||
# the ruling block rather than read again — and handed HERE, before the
|
||||
# assigned-flag comment and the evidence nudge below, so neither wait is
|
||||
# ever answered by anything this pass writes. `post-merge` +
|
||||
# `needs-ruling` now costs one comments read where it cost three.
|
||||
ruling_age="$evidence_age"
|
||||
if [ "$assignees" -gt 0 ] || has_issue_label attention; then
|
||||
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.'
|
||||
|
|
@ -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
|
||||
log "#$n: unstale (a ruling is pending)"
|
||||
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")"
|
||||
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")"
|
||||
fi
|
||||
reconcile_ruling "$n" "$age" "$NOW"
|
||||
reconcile_ruling "$n" "$ruling_age" "$NOW"
|
||||
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
|
||||
# Real activity only — comments, reviews, commits, never label churn, or
|
||||
# the sweep would reset its own clock. The nudge needs NO marker: the
|
||||
# Real activity only, as the caller's surface defines it: the PR sweep
|
||||
# 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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -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" \
|
||||
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 ------------------
|
||||
recent_timeline() {
|
||||
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 "" \
|
||||
grep -qF '#81: needs-triage (no queue state)' <<<"$broken_mint"
|
||||
|
||||
# -- 3. the blockers->ready flip, then a failed TIMELINE read ----------------
|
||||
# The wider class: the failing read is the second one inside
|
||||
# last_issue_activity, so the comments read answers and the marker check and
|
||||
# the flip both complete first. A comment AND a label edit are staged.
|
||||
# -- 3. the blockers->ready flip, then a failed COMMENTS read ----------------
|
||||
# The read that guards this branch is the marker check ahead of the parse
|
||||
# echo: a broken comments endpoint skips there, before the echo or the flip
|
||||
# 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"}' \
|
||||
>"$ORDER/repos_owner_repo_issues_82.json"
|
||||
order_fixture 83 '[{"name":"blocked"},{"name":"needs-ruling"}]' 'Blocked by #82.'
|
||||
order_board '[{"number":83}]'
|
||||
order_heals 83 comments
|
||||
order_heals 83 timeline
|
||||
healthy_flip="$(order_run)"
|
||||
check "the control: a healthy pass really does flip cleared blockers to ready" 0 \
|
||||
"issueflow: #83: blockers closed -> ready" printf '%s\n' "$healthy_flip"
|
||||
check "...writing the label edit" 0 "" order_wrote 83 edit
|
||||
check "...and posting the blockers-cleared comment" 0 "" order_wrote 83 comment
|
||||
order_breaks 83 timeline
|
||||
order_breaks 83 comments
|
||||
broken_flip="$(order_run)"
|
||||
check "a failed timeline read skips the blockers->ready composition" 0 \
|
||||
"issueflow: #83: skipped this pass — could not read its activity history: $GH_STUB_STDERR" \
|
||||
check "a failed comments read skips the blockers->ready composition" 0 \
|
||||
"issueflow: #83: skipped this pass — could not read its comments: $GH_STUB_STDERR" \
|
||||
printf '%s\n' "$broken_flip"
|
||||
check "...leaving the issue blocked" 1 "" order_wrote 83 edit
|
||||
check "...with no comment posted about it" 1 "" order_wrote 83 comment
|
||||
check "...and nothing claiming the flip happened" 1 "" \
|
||||
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 -------------------------
|
||||
# The comment-only half of the class: an epic nudge is staged, and the
|
||||
# ruling tail's activity read fails after it. A comment is as much a
|
||||
# mutation as a label — it is the thing markers exist to make idempotent.
|
||||
# -- 4. a posted nudge, then a failed COMMENTS read -------------------------
|
||||
# The comment-only half of the class: the epic nudge's own marker check is
|
||||
# the read that fails, so the nudge is never staged and the skip reports the
|
||||
# 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"}]' \
|
||||
'## Task list
|
||||
|
||||
- [x] #82'
|
||||
order_board '[{"number":84}]'
|
||||
order_heals 84 comments
|
||||
order_heals 84 timeline
|
||||
healthy_nudge="$(order_run)"
|
||||
check "the control: a healthy pass really does nudge a completed epic" 0 \
|
||||
"issueflow: #84: completed epic nudged" printf '%s\n' "$healthy_nudge"
|
||||
check "...by posting a comment" 0 "" order_wrote 84 comment
|
||||
order_breaks 84 timeline
|
||||
order_breaks 84 comments
|
||||
broken_nudge="$(order_run)"
|
||||
check "a failed timeline read skips the epic-nudge composition" 0 \
|
||||
"issueflow: #84: skipped this pass — could not read its activity history: $GH_STUB_STDERR" \
|
||||
check "a failed comments read skips the epic-nudge composition" 0 \
|
||||
"issueflow: #84: skipped this pass — could not read its comments: $GH_STUB_STDERR" \
|
||||
printf '%s\n' "$broken_nudge"
|
||||
check "...and the nudge comment is never posted" 1 "" order_wrote 84 comment
|
||||
check "...and nothing claims it was" 1 "" \
|
||||
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) --
|
||||
check "a mutation-bearing composition that skips is still not a crash" 1 "" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue