Skip to content

feat(replay_soak): PR gate consecutive-green ≥ 7d + docs (#403 C) - #424

Closed
yoshi280 wants to merge 0 commit into
mainfrom
feat/issue-403-replay-soak-C
Closed

feat(replay_soak): PR gate consecutive-green ≥ 7d + docs (#403 C)#424
yoshi280 wants to merge 0 commit into
mainfrom
feat/issue-403-replay-soak-C

Conversation

@yoshi280

@yoshi280 yoshi280 commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Closes deliverable C public-repo scope of #403. Companion to PR #423 (deliverable A).

Implements the operator ratification at #403 (comment) (2026-05-04T19:54:28Z): committed JSONL status file consumer + PR check.

What ships

  • scripts/replay_soak_streak.py — pure script that walks the tail of .replay-soak-status.json and counts consecutive rows where replay_full_equality_result == "pass" AND mismatched + derived_orphan == 0. Both conditions required — a row that claims pass but has nonzero drift counters still breaks the streak. Exit 0 if streak ≥ threshold (default 7), exit 1 otherwise, exit 2 on malformed file. Six tests in tests/test_replay_soak_streak.py.

  • .github/workflows/replay-soak-gate.yml — fires on every PR; produces the check consecutive-green ≥ 7d. Per the ratification, this name is what gets added to the branch ruleset's required_status_checks list (admin step, separate from this PR). The workflow checks out main (not the PR branch) so it reads the authoritative status file — the cron only writes on main.

  • docs/v2_replay.md — new "Soak gate (operational definition — [v2.x] Tighten replay-soak gate — operationalize #262's '≥1 week green' as actual CI probe #403)" section anchors the canonical "≥ 1 week green" definition: .replay-soak-status.json + the consecutive-green check. Supersedes any prior comment-text calendar arithmetic ("soak-locked through YYYY-MM-DD" notes).

Dependencies

  • PR feat(replay_soak): v0.1 public fixture + daily soak workflow (#403 A) #423 must merge first for .replay-soak-status.json to exist on main. Until then this PR's gate workflow correctly fails because there's no streak to read.
  • Admin ruleset edit — adding consecutive-green ≥ 7d to the branch ruleset's required-checks list. Out of scope for this PR; needs the robotrocketscience identity (yoshi280 has admin: false).

Out of scope

  • aelf-claim.sh STALE-check rewrite — sister change in the HOME repo (~/.claude/scripts/aelf-claim.sh) per the two-repo workflow. Ships separately; this PR is the public-repo half of deliverable C.
  • First-week-of-data behavior: until the cron has accumulated 7 entries on main, every PR's gate fails. That's the intended behavior — the check is fail-safe before evidence exists.

Test plan

  • uv run pytest tests/test_replay_soak_streak.py -v — 6 passed.
  • uv run pytest --ignore=tests/bench_gate -q — 2440 passed (+6 new), 22 skipped.
  • Smoke: python3 scripts/replay_soak_streak.py --status-file <empty> --threshold 7 → exit 1 (streak=0).
  • After PR feat(replay_soak): v0.1 public fixture + daily soak workflow (#403 A) #423 merges + the cron runs ≥ 7 days: re-validate gate flips to green on a no-op PR.

Summary by Sourcery

Introduce a replay soak merge gate based on a computed consecutive green streak and document the operational definition of the soak window.

New Features:

  • Add a replay soak streak computation script that reads the JSONL status file and reports consecutive green runs with configurable threshold.
  • Introduce a GitHub Actions workflow that runs on pull requests to enforce a minimum 7-day consecutive green replay soak streak before merges touching replay behavior.

Enhancements:

  • Define and document the canonical operational soak gate criteria in the replay docs, tying it to the JSONL status file and streak-based PR check.
  • Add unit tests covering edge and failure cases for the replay soak streak calculation and status file parsing.

Documentation:

  • Document the soak gate operational definition, its data source, and the associated PR check in the replay documentation.

Tests:

  • Add tests for replay soak streak calculation, including empty and missing files, streak breaks on failures or drift, and malformed JSON handling.

@sourcery-ai

sourcery-ai Bot commented May 5, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a replay soak "consecutive green ≥ 7 days" merge gate implemented as a JSONL status file consumer script, a GitHub Actions PR workflow that enforces the gate against main’s status file, and documentation that defines this soak gate as the canonical operational definition.

Sequence diagram for PR replay soak gate check execution

sequenceDiagram
  actor Developer
  participant GitHub
  participant PR_Gate_Workflow as replay-soak-gate.yml
  participant Main_Repo as main_repo_checkout
  participant Status_File as replay_soak_status_jsonl
  participant Streak_Script as replay_soak_streak_py
  participant Branch_Protection as branch_protection_rules

  Developer->>GitHub: open/synchronize PR targeting main
  GitHub->>PR_Gate_Workflow: trigger pull_request event

  PR_Gate_Workflow->>Main_Repo: actions/checkout ref=main
  Main_Repo-->>PR_Gate_Workflow: workspace with .replay-soak-status.json (if exists)

  PR_Gate_Workflow->>Status_File: check existence
  alt status file missing
    PR_Gate_Workflow-->>PR_Gate_Workflow: set streak=0, emit warning
  else status file present
    PR_Gate_Workflow->>Streak_Script: run python3 scripts/replay_soak_streak.py --quiet
    Streak_Script->>Status_File: load_rows()
    Status_File-->>Streak_Script: JSONL rows
    Streak_Script-->>PR_Gate_Workflow: print streak n, exit 0/1/2
  end

  PR_Gate_Workflow-->>GitHub: set output streak=n, log notice

  alt streak < 7
    PR_Gate_Workflow-->>GitHub: job failure, check status failure
  else streak ≥ 7
    PR_Gate_Workflow-->>GitHub: job success, check status success
  end

  GitHub->>Branch_Protection: evaluate required check consecutive-green ≥ 7d
  Branch_Protection-->>Developer: allow or block merge of PR
Loading

File-Level Changes

Change Details Files
Introduce a reusable script that computes the replay-soak consecutive green streak from the JSONL status file and exposes it via CLI for CI use.
  • Add streak() to count consecutive trailing rows where replay_full_equality_result == "pass" and mismatched + derived_orphan == 0, walking backward from the file tail.
  • Add load_rows() to read an append-only JSONL file, skipping blank lines, validating that each non-empty line is a JSON object, and exiting with an error on malformed JSON.
  • Implement a CLI entrypoint with arguments for status file path, threshold, and quiet mode, printing the streak and returning exit codes 0 (streak ≥ threshold), 1 (below threshold), or 2 (malformed file).
scripts/replay_soak_streak.py
Add unit tests that pin the streak semantics and error-handling behavior on realistic edge cases.
  • Test that empty and missing status files yield a streak of 0 without error.
  • Test correct streak counts for exactly seven consecutive passes, streak breaking on a fail row, and streak breaking on drift counters despite a "pass" result.
  • Test that a malformed JSONL line triggers SystemExit from load_rows, mapping to exit code 2 in main().
tests/test_replay_soak_streak.py
Introduce a PR GitHub Actions workflow that runs on main’s status file and fails the PR when the streak is below 7 days or the status file is missing.
  • Define a pull_request-triggered workflow with hardened runner, read-only contents permissions, and per-PR concurrency control.
  • Check out main (not the PR branch), compute the streak using the replay_soak_streak script when the status file exists, or treat it as 0 with a warning when it does not.
  • Expose the streak via step outputs, log it as a notice, and enforce a hard threshold of 7 with an error message that explains the gate behavior on failure.
.github/workflows/replay-soak-gate.yml
Document the soak gate as the canonical operational definition and link it into the replay design’s provenance chain.
  • Add a new section that defines the soak gate in terms of the .replay-soak-status.json JSONL file, its schema, and the rule of ≥ 7 consecutive rows with zero drift and pass results, enforced by the replay-soak / consecutive-green ≥ 7d PR check.
  • Clarify that this definition supersedes previous comment-based calendar arithmetic and reference the corpus fixture and schema README as the public-side input and contract.
  • Update the provenance chain to insert [v2.x] Tighten replay-soak gate — operationalize #262's '≥1 week green' as actual CI probe #403 as the soak gate operationalization between existing upstream items.
docs/v2_replay.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 59 minutes and 55 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 355fa5d6-414a-4c9a-9b05-e8ac2918ee5f

📥 Commits

Reviewing files that changed from the base of the PR and between cf00de3 and 7a98607.

📒 Files selected for processing (4)
  • .github/workflows/replay-soak-gate.yml
  • docs/v2_replay.md
  • scripts/replay_soak_streak.py
  • tests/test_replay_soak_streak.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-403-replay-soak-C

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread .github/workflows/replay-soak-gate.yml Fixed
Comment thread .github/workflows/replay-soak-gate.yml Fixed
- name: Enforce threshold
run: |
set -euo pipefail
n="${{ steps.streak.outputs.streak }}"

import argparse
import json
import sys

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In scripts/replay_soak_streak.py, load_rows raising SystemExit makes it awkward to reuse programmatically (and forces the tests to expect SystemExit); consider raising a regular exception (e.g. a custom ReplaySoakError) and mapping that to exit code 2 only in main().
  • The liberal list[dict] # type: ignore[type-arg] usage in scripts/replay_soak_streak.py and tests makes the data shape opaque to tooling; defining a TypedDict (or at least dict[str, Any]) for the status rows would remove the ignores and clarify expected fields.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `scripts/replay_soak_streak.py`, `load_rows` raising `SystemExit` makes it awkward to reuse programmatically (and forces the tests to expect `SystemExit`); consider raising a regular exception (e.g. a custom `ReplaySoakError`) and mapping that to exit code 2 only in `main()`.
- The liberal `list[dict]  # type: ignore[type-arg]` usage in `scripts/replay_soak_streak.py` and tests makes the data shape opaque to tooling; defining a TypedDict (or at least `dict[str, Any]`) for the status rows would remove the ignores and clarify expected fields.

## Individual Comments

### Comment 1
<location path="scripts/replay_soak_streak.py" line_range="24-35" />
<code_context>
+from pathlib import Path
+
+
+def streak(rows: list[dict]) -> int:  # type: ignore[type-arg]
+    n = 0
+    for row in reversed(rows):
+        if row.get("replay_full_equality_result") != "pass":
+            break
+        if int(row.get("mismatched", 0)) + int(row.get("derived_orphan", 0)) != 0:
+            break
+        n += 1
+    return n
+
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Handle non-integer `mismatched` / `derived_orphan` values explicitly so malformed rows become exit 2 instead of a generic failure.

`int(row.get("mismatched", 0)) + int(row.get("derived_orphan", 0))` will raise `ValueError` if those fields are present but non-numeric, causing an unhandled exception and exit code 1. To align with the existing behavior for malformed JSON/shape (exit 2), consider catching `ValueError` (here or in `load_rows`) and mapping it to the same exit-2 path so all malformed status files are treated consistently.

```suggestion
from pathlib import Path


def streak(rows: list[dict]) -> int:  # type: ignore[type-arg]
    n = 0
    for row in reversed(rows):
        if row.get("replay_full_equality_result") != "pass":
            break

        try:
            mismatched = int(row.get("mismatched", 0))
            derived_orphan = int(row.get("derived_orphan", 0))
        except (TypeError, ValueError) as exc:
            # Treat non-integer fields as a malformed status file so we exit 2,
            # consistent with JSON/shape errors.
            print(
                "malformed status file: non-integer mismatched/derived_orphan",
                file=sys.stderr,
            )
            raise SystemExit(2) from exc

        if mismatched + derived_orphan != 0:
            break
        n += 1
    return n
```
</issue_to_address>

### Comment 2
<location path=".github/workflows/replay-soak-gate.yml" line_range="39-50" />
<code_context>
+            echo "::warning::.replay-soak-status.json does not exist on main yet; treating streak as 0"
+            n=0
+          else
+            n=$(python3 scripts/replay_soak_streak.py --quiet)
+          fi
+          echo "streak=$n" >> "$GITHUB_OUTPUT"
</code_context>
<issue_to_address>
**suggestion:** Consider pinning the Python version in the workflow for reproducibility.

This step depends on whatever `python3` happens to be on `ubuntu-latest`, which can change as the image updates. Please add an `actions/setup-python` step and run this with an explicitly pinned version (e.g., 3.11) to keep CI behavior aligned with your local target Python version.

```suggestion
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Compute streak
        id: streak
        run: |
          set -euo pipefail
          if [ ! -f .replay-soak-status.json ]; then
            echo "::warning::.replay-soak-status.json does not exist on main yet; treating streak as 0"
            n=0
          else
            n=$(python scripts/replay_soak_streak.py --quiet)
          fi
          echo "streak=$n" >> "$GITHUB_OUTPUT"
          echo "::notice::replay-soak consecutive-green streak: $n"
```
</issue_to_address>

### Comment 3
<location path="tests/test_replay_soak_streak.py" line_range="101-105" />
<code_context>
+    assert replay_soak_streak.streak(replay_soak_streak.load_rows(p)) == 1
+
+
+def test_malformed_jsonl_raises(tmp_path: Path) -> None:
+    """Hypothesis: a non-JSON line raises SystemExit (exit code 2 in main)."""
+    p = tmp_path / "status.json"
+    p.write_text("not-json\n")
+    with pytest.raises(SystemExit):
+        replay_soak_streak.load_rows(p)
</code_context>
<issue_to_address>
**suggestion (testing):** Also cover the non-dict JSON row case that `load_rows` explicitly guards against

You already cover malformed JSON text. Please also add a test where the JSON parses successfully but is not an object (e.g. a line containing `"42\n"` or `"[]\n"`), and assert that `load_rows` raises `SystemExit` for that line. This will exercise the additional guard and the second exit-code-2 failure path in `main()`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +24 to +35
from pathlib import Path


def streak(rows: list[dict]) -> int: # type: ignore[type-arg]
n = 0
for row in reversed(rows):
if row.get("replay_full_equality_result") != "pass":
break
if int(row.get("mismatched", 0)) + int(row.get("derived_orphan", 0)) != 0:
break
n += 1
return n

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Handle non-integer mismatched / derived_orphan values explicitly so malformed rows become exit 2 instead of a generic failure.

int(row.get("mismatched", 0)) + int(row.get("derived_orphan", 0)) will raise ValueError if those fields are present but non-numeric, causing an unhandled exception and exit code 1. To align with the existing behavior for malformed JSON/shape (exit 2), consider catching ValueError (here or in load_rows) and mapping it to the same exit-2 path so all malformed status files are treated consistently.

Suggested change
from pathlib import Path
def streak(rows: list[dict]) -> int: # type: ignore[type-arg]
n = 0
for row in reversed(rows):
if row.get("replay_full_equality_result") != "pass":
break
if int(row.get("mismatched", 0)) + int(row.get("derived_orphan", 0)) != 0:
break
n += 1
return n
from pathlib import Path
def streak(rows: list[dict]) -> int: # type: ignore[type-arg]
n = 0
for row in reversed(rows):
if row.get("replay_full_equality_result") != "pass":
break
try:
mismatched = int(row.get("mismatched", 0))
derived_orphan = int(row.get("derived_orphan", 0))
except (TypeError, ValueError) as exc:
# Treat non-integer fields as a malformed status file so we exit 2,
# consistent with JSON/shape errors.
print(
"malformed status file: non-integer mismatched/derived_orphan",
file=sys.stderr,
)
raise SystemExit(2) from exc
if mismatched + derived_orphan != 0:
break
n += 1
return n

Comment on lines +39 to +50
- name: Compute streak
id: streak
run: |
set -euo pipefail
if [ ! -f .replay-soak-status.json ]; then
echo "::warning::.replay-soak-status.json does not exist on main yet; treating streak as 0"
n=0
else
n=$(python3 scripts/replay_soak_streak.py --quiet)
fi
echo "streak=$n" >> "$GITHUB_OUTPUT"
echo "::notice::replay-soak consecutive-green streak: $n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider pinning the Python version in the workflow for reproducibility.

This step depends on whatever python3 happens to be on ubuntu-latest, which can change as the image updates. Please add an actions/setup-python step and run this with an explicitly pinned version (e.g., 3.11) to keep CI behavior aligned with your local target Python version.

Suggested change
- name: Compute streak
id: streak
run: |
set -euo pipefail
if [ ! -f .replay-soak-status.json ]; then
echo "::warning::.replay-soak-status.json does not exist on main yet; treating streak as 0"
n=0
else
n=$(python3 scripts/replay_soak_streak.py --quiet)
fi
echo "streak=$n" >> "$GITHUB_OUTPUT"
echo "::notice::replay-soak consecutive-green streak: $n"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Compute streak
id: streak
run: |
set -euo pipefail
if [ ! -f .replay-soak-status.json ]; then
echo "::warning::.replay-soak-status.json does not exist on main yet; treating streak as 0"
n=0
else
n=$(python scripts/replay_soak_streak.py --quiet)
fi
echo "streak=$n" >> "$GITHUB_OUTPUT"
echo "::notice::replay-soak consecutive-green streak: $n"

Comment on lines +101 to +105
def test_malformed_jsonl_raises(tmp_path: Path) -> None:
"""Hypothesis: a non-JSON line raises SystemExit (exit code 2 in main)."""
p = tmp_path / "status.json"
p.write_text("not-json\n")
with pytest.raises(SystemExit):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Also cover the non-dict JSON row case that load_rows explicitly guards against

You already cover malformed JSON text. Please also add a test where the JSON parses successfully but is not an object (e.g. a line containing "42\n" or "[]\n"), and assert that load_rows raises SystemExit for that line. This will exercise the additional guard and the second exit-code-2 failure path in main().

@github-actions github-actions Bot added the attn:merge-conflict PR branch needs rebase label May 5, 2026
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

This PR is now behind main. Rebase locally so your commit signatures stay intact:

git fetch origin && git checkout 'feat/issue-403-replay-soak-C' && git rebase origin/main
# resolve conflicts if any, then
git push --force-with-lease

Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the required_signatures rule on main then blocks the merge. See #341.

@robotrocketscience
robotrocketscience force-pushed the feat/issue-403-replay-soak-C branch from 7a98607 to e1c3dcb Compare May 5, 2026 06:43
@yoshi280

yoshi280 commented May 5, 2026

Copy link
Copy Markdown
Collaborator Author

Landed via FF-push to main outside the PR mechanism (same admin path as #409 — required-signatures rule blocks GitHub-side rebase merge for SSH-signed commits). Commits e1c3dcb / 13c1837 / 94ac68a are on main, all SSH-signed G. PR auto-closed but mergedAt is null because GitHub only records merges that go through the PR merge button.

Effectively merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:merge-conflict PR branch needs rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants