Skip to content

update-shape.py: apply puts every byte back under any OSError after its first write, says what it put back on stderr, and raises the error as it was, so a file gone between its stat and its read, a broken pipe or an EIO no longer leaves the tree half-written behind a traceback (#1173) - #1187

Merged
brettheap merged 2 commits into
mainfrom
l3-1173-apply-rolls-back-any-oserror
Oct 23, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What changed

A follow-up outside v1.2.0 (Brett Heap, 2026-10-21T10:32:04Z). It lands only after the v1.2.0 tag, on a later merge word.

update-shape.py apply rolled back an OSError after its first write only where the system had refused a read (#976). Any other one was raised as it was, with every copy, the pin and the manifest left written: a file gone between its stat and its read, an interpreter or a working directory gone when a validator starts, an error that names no path (a BrokenPipeError from a flush into a pipe nobody reads any more, an EIO). The traceback sat over a half-written tree and said nothing of what was written, the outcome Rollback.writing (#285) and _rewrite_readme (#152) exist to prevent. It now puts every byte back first.

update-shape.py:

  • cmd_apply's except OSError arm, where unreadable_refusal answers None, calls the new _roll_back_before_raising(ledger) and then re-raises with a bare raise. The three-arm shape is kept: a Refusal or a CommandFailed still goes to _rolled_back, and a refused read is still made update-unreadable and rolled back under it, with update-shape.py: a path in the root or the upstream this user cannot read is update-unreadable on every Python, so a scripts/ it can list but not search is no longer five copy-missing and a validator it cannot stat is never SKIPPED #976's rows and messages as they were.
  • _roll_back_before_raising(ledger) asks ledger.undo() and, when it returns None (no write had been made, or the commit landed and is the result), says nothing and returns. Otherwise it flushes stdout, letting an OSError from the flush go, and prints what was put back on stderr, in _rolled_back's own words.
  • Three docstrings that said apply rolls back only on a Refusal (and a CommandFailed), which an OSError after the first write is not (run_validator, Rollback.writing, _rewrite_readme), are put in the past tense, and _rewrite_readme's says what is still true: an OSError the tool cannot name is a traceback, which names no step and no remedy.

tests/test_update_shape_unreadable.py, beside test_any_read_refused_after_the_writes_rolls_them_back (6 rows added, 54 in the file):

  • test_any_oserror_after_the_writes_rolls_them_back_and_is_raised[a-file-gone | a-broken-pipe | EIO-naming-no-path]: _refuse_red_validators made to raise a FileNotFoundError naming a path, a BrokenPipeError and an OSError(errno.EIO, ...) with no path. Each leaves the tree byte for byte as it was, says on stderr (and only there) what was put back, and raises the same error object.
  • test_a_stdout_nobody_reads_is_rolled_back_under_its_broken_pipe: no stand-in. The tool runs as a subprocess with its stdout a pipe whose reader is gone, as apply --yes | head -1 leaves it. The tree is as it was, and what went back is on stderr above the traceback.
  • test_an_oserror_before_the_first_write_is_raised_and_says_nothing and test_an_oserror_after_the_commit_puts_nothing_back: the two ends where there is nothing to put back. The error is raised as it always was, stderr is empty, and after the commit the commit and its tree stand.

Decided where the Direction left it open

  • A helper of its own, not _rolled_back. _rolled_back ends in raise of a refusal or return 2, and this arm has to re-raise the original error with its traceback. The helper does the first half (undo, say) and leaves the raise to the arm.
  • Nothing to put back says nothing. An OSError before the first write, or after the commit landed, is raised exactly as it always was (Rollback.undo returns None in both).
  • The stdout flush is let go. _rolled_back flushes stdout so the copied lines come before the rollback in a merged log. Here the error being answered may be stdout's own, a pipe nobody reads, and an unguarded flush would raise over what must still reach stderr. contextlib.suppress(OSError) lets it go, and the original error is the one raised. M3 puts the bare flush back.
  • Said on stderr, as the Direction says and as _rolled_back does for a failed git command. M6 prints it on stdout.
  • The error is raised, not made a refusal. The Direction's own words: the traceback stays, as the tool's answer to an error it cannot name, and the tree is as it was under it.

Left out of scope

Measured

A scratch probe (not in the repository) runs update-shape.py apply --root <root> --yes --upstream <local clone> --at B, with a LOCAL --upstream, a stub gh and no network. In process, _refuse_red_validators raises the named OSError after the writes, and once with no stand-in at all: the tool as a subprocess with its stdout a closed pipe. It prints what main() raised, what stderr held, and what the root holds afterwards.

Before, at 961640c6 (the first and the fourth of the four cases):

=== in process, FileNotFoundError-naming-a-path after the writes ===
  stdout lines naming a write:
      copied   scripts/validate-manifest.py
      re-pinned contracts/shape-pin.yaml and project.yaml 961640c681b5 -> 3baea52ba2ae
  main(): RAISED FileNotFoundError: [Errno 2] No such file or directory: '<root>/scripts/validate-pins.py'
  stderr:
    (empty)
  pin commit: B, RE-PINNED
  scripts/validate-manifest.py: WRITTEN
  git status --porcelain:
     M contracts/shape-pin.yaml
     M project.yaml
     M scripts/validate-manifest.py
=== subprocess, stdout a closed pipe ===
  exit 120
  stderr:
    Traceback (most recent call last):
      ...
        sys.stdout.flush()
    BrokenPipeError: [Errno 32] Broken pipe
  pin commit: B, RE-PINNED
  scripts/validate-manifest.py: WRITTEN

BrokenPipeError and OSError(EIO) after the writes read the same as the first: stderr empty, pin at B, the copy written, three files modified.

After, at 7a9d397b:

=== in process, FileNotFoundError-naming-a-path after the writes ===
  main(): RAISED FileNotFoundError: [Errno 2] No such file or directory: '<root>/scripts/validate-pins.py'
  stderr:
    ROLLED BACK: every byte this command wrote has been put back, and the tree is exactly as it was:
        restored  scripts/validate-manifest.py
        restored  contracts/shape-pin.yaml
        restored  project.yaml
  pin commit: A, as before
  scripts/validate-manifest.py: as before
  git status --porcelain:
    (clean)
=== subprocess, stdout a closed pipe ===
  exit 120
  stderr:
    ROLLED BACK: every byte this command wrote has been put back, and the tree is exactly as it was:
        restored  scripts/validate-manifest.py
        restored  contracts/shape-pin.yaml
        restored  project.yaml
    Traceback (most recent call last):
      ...
        sys.stdout.flush()
    BrokenPipeError: [Errno 32] Broken pipe
  pin commit: A, as before
  scripts/validate-manifest.py: as before

BrokenPipeError and OSError(EIO) read the same as the first: the three files restored, the pin at A, the status clean. The error raised is the same in each; exit 120 is the interpreter's own code for a stdout that would not flush at exit, as before.

The out-of-scope find, measured at 7a9d397b by a second probe: the pin made read-only so the re-pin is refused update-write-failed after the copy. With stdout read, stderr holds REFUSED update-write-failed: ... and ROLLED BACK: ... restored scripts/validate-manifest.py, exit 2. With stdout a closed pipe:

  exit 120
  stderr:
    Traceback (most recent call last):
      ...
    repo_shape.Refusal: REFUSED update-write-failed: re-pinning contracts/shape-pin.yaml and project.yaml failed at contracts/shape-pin.yaml: Permission denied
    ...
    During handling of the above exception, another exception occurred:
    ...
      File ".../update-shape.py", line 5236, in _rolled_back
        sys.stdout.flush()
    BrokenPipeError: [Errno 32] Broken pipe
  pin commit: A, as before
  scripts/validate-manifest.py: as before

The tree is back; the refusal's own REFUSED line and its ROLLED BACK list never reach stderr.

Tests

Each file runs alone, on Python 3.12.3, as timeout 1500 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-gate -o tmp_path_retention_policy=failed <file>. A stub gh that exits 97 is first on PATH, with GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL a scratch file that sets only gc.auto and maintenance.auto, and EMAIL set. Every run exited 0.

The guards the brief names come first, then the update-shape files, then the other files that name update-shape.py and read what apply raises or prints. tests/test_shape_doctor.py is not run: shape-doctor.py is not changed.

file result
tests/test_update_shape_unreadable.py 54 passed in 15.78s
tests/test_repo_hygiene.py 101 passed in 2.97s
tests/test_path_guard_hygiene.py 49 passed in 15.18s
tests/test_no_auto_maintenance.py 58 passed, 2 skipped in 9.78s
tests/test_agents_md_codes.py 47 passed in 2.17s
tests/test_cli_reference.py 23 passed in 34.56s
tests/test_update_add_codes_documented.py 6 passed in 0.50s
tests/test_update_shape_partial_rollback.py 22 passed in 13.47s
tests/test_update_shape_messages.py 26 passed in 26.89s
tests/test_update_shape_apply_lines_and_headers.py 42 passed in 25.54s
tests/test_update_shape.py 90 passed in 97.26s (0:01:37)
tests/test_update_shape_landing.py 74 passed in 79.69s (0:01:19)
tests/test_update_shape_rewrite_symlink.py 29 passed in 17.02s
tests/test_update_shape_add_symlink.py 38 passed, 3 skipped in 22.63s
tests/test_update_shape_add_not_a_directory.py 13 passed in 14.35s
tests/test_update_shape_add_ignored.py 26 passed in 39.22s
tests/test_update_shape_add.py 20 passed in 22.33s
tests/test_update_shape_branch_any_directory.py 28 passed in 10.31s
tests/test_update_shape_hardlink.py 24 passed in 24.41s
tests/test_update_shape_header_comments.py 54 passed in 23.92s
tests/test_update_shape_kept_helper.py 159 passed in 32.48s
tests/test_update_shape_merged_away.py 18 passed in 74.88s (0:01:14)
tests/test_update_shape_next_quoting.py 49 passed in 75.05s (0:01:15)
tests/test_update_shape_paste_lines.py 28 passed in 20.25s
tests/test_update_shape_pr_base.py 29 passed in 40.46s
tests/test_doctor_unreadable_paths.py 34 passed in 29.61s
tests/test_doctor_unreadable_pin.py 52 passed in 32.89s
tests/test_bom_reads.py 25 passed in 13.26s
tests/test_bom_rewrites.py 12 passed in 0.47s
tests/test_stdout_encoding.py 47 passed, 1 skipped in 19.20s
tests/test_windows_paths.py 46 passed in 4.34s
tests/test_validator_child_encoding.py 163 passed in 53.21s
tests/test_pin_rewrite_layout.py 115 passed in 1.42s
tests/test_openreposhape_package.py 213 passed in 40.13s
tests/test_commit_trailers.py 158 passed in 0.68s
tests/test_refusal_default_remediation.py 20 passed in 15.23s
tests/test_chain_remedy_callers.py 68 passed in 11.86s
tests/test_pinned_copy_text.py 87 passed in 8.67s

Mutants

Each mutant breaks one behaviour of update-shape.py once and runs tests/test_update_shape_unreadable.py -k "oserror or nobody_reads" (the 6 new rows). The file is then restored from a saved copy and checked with cmp (exit 0 each time). Run twice, by the writer before the commit and again at the head, with the same verdicts.

mutant what it breaks verdict killed by restored
M1-arm-as-at-main-no-rollback the arm as it stands at main: if refusal is None: raise, no rollback KILLED test_any_oserror_after_the_writes_rolls_them_back_and_is_raised[a-file-gone], [a-broken-pipe], [EIO-naming-no-path], test_a_stdout_nobody_reads_is_rolled_back_under_its_broken_pipe identical
M2-rolled-back-but-not-said the undo kept, the print(undone, file=sys.stderr) removed KILLED the same four identical
M3-flush-not-let-go the guarded flush made a bare sys.stdout.flush() KILLED test_a_stdout_nobody_reads_is_rolled_back_under_its_broken_pipe identical
M4-error-swallowed-exit-2 the arm returns 2 after the rollback instead of re-raising KILLED the three ...is_raised rows, test_a_stdout_nobody_reads_..., test_an_oserror_before_the_first_write_is_raised_and_says_nothing, test_an_oserror_after_the_commit_puts_nothing_back identical
M5-says-something-with-nothing-back the if undone is None: return removed, so nothing-to-put-back still prints KILLED test_an_oserror_before_the_first_write_is_raised_and_says_nothing, test_an_oserror_after_the_commit_puts_nothing_back identical
M6-said-on-stdout print(undone) in place of print(undone, file=sys.stderr) KILLED the three ...is_raised rows and test_a_stdout_nobody_reads_... identical

Merges

git merge-tree --write-tree of this head (cc8a87ee) with origin/main (961640c6, frozen until the v1.2.0 tag) and with each open PR that touches update-shape.py or tests/test_update_shape_unreadable.py, at the heads GitHub lists now. The open PRs were re-listed live after the fix commit (48 open); no PR other than these touches either file.

origin/main rc=0 clean tree=83e8610bced9bfbba92f1b53aa2b08d0cc942ad1
#1166 (2593952c, update-shape.py and tests/test_update_shape_unreadable.py) rc=0 clean tree=c87805de515a9edfde08fc638e20d82d9932c8ec
#1133 (a7bfee1b, update-shape.py and tests/test_update_shape_kept_helper.py) rc=0 clean tree=408ab802f04e2610e3db525916040901868b183f
#1188 (aa0275be, tests/test_bom_tool_io.py only; it is #1176) rc=0 clean tree=8570eae64e3c81c306b0c875aa20d33b6ffaee84

All are clean. #1166 edits the same test file, in other rows, and merges without conflict. #1176 is #1188: it touches only tests/test_bom_tool_io.py, no file this change touches.

Fix round

RV1187's review (ruling 8, READY, no MUST) of 7a9d397b asked for two one-line test changes and a body fix. One new commit, cc8a87ee, touches tests/test_update_shape_unreadable.py alone; update-shape.py is byte for byte as it was.

Re-run at cc8a87ee, one pytest at a time, each under the stub-gh host rules:

run result
tests/test_update_shape_unreadable.py with tests/test_update_shape.py and tests/test_update_shape_add_ignored.py, in one pytest 170 passed in 92.25s (0:01:32)
tests/test_path_guard_hygiene.py 49 passed in 6.29s
tests/test_repo_hygiene.py 101 passed in 1.36s
tests/test_no_auto_maintenance.py 58 passed, 2 skipped in 4.75s

Mutants M1 to M6 and M8, each run against the whole of tests/test_update_shape_unreadable.py (54 rows), the file restored from a saved copy and checked with cmp (exit 0 each):

mutant verdict killed by
M1, the arm as at main: if refusal is None: raise, no rollback KILLED (4 failed, 50 passed) the three ...is_raised rows and test_a_stdout_nobody_reads_...
M2, the rollback kept, the print(undone, file=sys.stderr) removed KILLED (4 failed, 50 passed) the same four
M3, the guarded flush made a bare sys.stdout.flush() KILLED (1 failed, 53 passed) test_a_stdout_nobody_reads_...
M4, the arm returns 2 after the rollback instead of re-raising KILLED (6 failed, 48 passed) the three ...is_raised rows, test_a_stdout_nobody_reads_..., test_an_oserror_before_the_first_write_..., test_an_oserror_after_the_commit_...
M5, the if undone is None: return removed KILLED (2 failed, 52 passed) test_an_oserror_before_the_first_write_..., test_an_oserror_after_the_commit_...
M6, print(undone) on stdout KILLED (4 failed, 50 passed) the three ...is_raised rows and test_a_stdout_nobody_reads_...
M8, the _roll_back_before_raising(ledger) call above if refusal is None (SURVIVED at 7a9d397b) KILLED (1 failed, 53 passed) test_any_read_refused_after_the_writes_rolls_them_back

Closes #1173

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

…er its first write, says what it put back on stderr, and raises the error as it was, so a file gone between its `stat` and its read, a broken pipe or an `EIO` no longer leaves the tree half-written behind a traceback (#1173)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes update-shape.py apply restore all writes after any mid-run OSError, safely report the rollback on stderr, and re-raise the original error, with focused tests covering named/pathless failures, closed stdout, and no-op rollback boundaries.

Sequence diagram for apply rollback after an OSError

sequenceDiagram
    participant Apply as cmd_apply
    participant Ledger as Rollback
    participant Stdout as stdout
    participant Stderr as stderr

    Apply->>Ledger: undo()
    alt writes exist
        Ledger-->>Apply: rollback message
        Apply->>Stdout: flush()
        Note over Stdout: OSError from flush is suppressed
        Apply->>Stderr: print(undone)
        Apply-->>Apply: raise original OSError
    else no writes or commit landed
        Ledger-->>Apply: None
        Apply-->>Apply: raise original OSError
    end
Loading

File-Level Changes

Change Details Files
Rollback now covers every mid-run OSError while preserving the original exception and traceback.
  • Route unrecognized OSErrors through a dedicated rollback helper before re-raising.
  • Undo writes, suppress stdout-flush failures, and report restored paths on stderr.
  • Remain silent when no writes need restoration, including failures before the first write or after commit.
  • Clarify affected rollback behavior and remaining traceback semantics in docstrings.
update-shape.py
Add coverage for rollback, output routing, exception identity, and no-op rollback boundaries.
  • Test named and pathless OSErrors after writes, including BrokenPipeError and EIO.
  • Exercise a real subprocess with closed stdout to verify rollback messaging survives a broken pipe.
  • Verify pre-write and post-commit OSErrors remain unchanged and produce no rollback message.
  • Assert the tree is byte-for-byte restored and the original error object is re-raised.
tests/test_update_shape_unreadable.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1173 After the first write, roll back every byte written when an OSError cannot be converted to an unreadable-path refusal, report what was restored on stderr, and re-raise the original error. ✅
#1173 Preserve the existing handling of Refusal, CommandFailed, and refused-read errors, and avoid reporting a rollback when an OSError occurs before any write or after the commit has landed. ✅

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

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-23T00:13:12Z) Independent review (RV1187, opus, ruling 8) of #1187 at head 7a9d397b, main 961640c6.

Verdict: READY. No MUST. Two SHOULDs, each a one-line test change measured below, are best taken in a fix round before the merge word, since this lands only after the v1.2.0 tag anyway.

The fix is right. Re-measured with my own probe (a LOCAL --upstream, stub gh, no network), in process unless said:

  • Errors after the writes: FileNotFoundError naming a path, BrokenPipeError, OSError(EIO), OSError(ENOSPC), and the validator's interpreter gone (a real subprocess.run, no stand-in).
    • At main, all five raise with stderr empty, the pin at B and three files modified.
    • At the head, all five restore the copy, the pin and the manifest, list them on stderr, raise the same object, and leave the status clean.
  • Subprocess, stdout a closed pipe: main exits 120 with the tree written. The head exits 120 with the ROLLED BACK list above the traceback and the tree as it was.
  • Subprocess, stdout and stderr one closed pipe (2>&1 | head -1): at main the tree is written; at the head it is as it was. Both exit 120.
  • Before the first write (EIO from _read_readme): raised as it was, stderr empty, tree untouched.
  • After the commit (--branch, next_line raising): raised as it was, stderr empty, and the commit and its tree stand.
  • Both ends are right, and both are pinned (M4, M5).

Added checks:

SHOULD 1, tests/test_update_shape_unreadable.py:716.

  • The pipe test inherits PYTHONUNBUFFERED. Exported as 1, stderr starts "REFUSED update-write-failed: ... Broken pipe" and the test fails (measured). tests/test_bootstrap_refusal_exit.py:180-187 blanks it for this reason.
  • Fix: env={**os.environ, "LANES_LANE": "", "PYTHONUNBUFFERED": ""}. Measured: it then passes under PYTHONUNBUFFERED=1 and still kills M3.

SHOULD 2, tests/test_update_shape_unreadable.py:646.

  • No row pins that a refused read still goes through _rolled_back alone, which the issue's Direction asks to keep. M8 (the _roll_back_before_raising call moved above if refusal is None) survives all 54 rows. It then prints the rollback list twice, once bare above REFUSED update-unreadable and once under it (measured).
  • Fix: after that line, add assert (out + err).count("ROLLED BACK") == 1, out + err. Measured: it passes at the head and kills M8.

NIT 1, the body. The #1176 lines ("has no PR yet", "the one line it edits in update-shape.py is in _repin") are stale. #1176 is now #1188 (aa0275be); it touches only tests/test_bom_tool_io.py, and merge-tree is clean (tree dc71966f).

NIT 2, update-shape.py:5276, optional. Guard the stderr print too (contextlib.suppress(OSError)), so that the original is always the error raised. Fine as is.

Mutants (my own script, the whole file per mutant, restored, cmp exit 0 each):

  • KILLED: M1 (the arm as at main), M2 (not said), M3 (a bare flush), M4 (exit 2, not raised), M5 (no early return), M6 (said on stdout).
  • SURVIVED: M7 (no flush at all), M8 (rollback above the refusal check: SHOULD 2), M9 (suppress only BrokenPipeError).
  • No test covers the merged-log order (M7) or the suppress's breadth (M9). The code is right on both, so no action is asked.

Tests at the head, one pytest at a time:

  • the PR's file with tests/test_update_shape.py and tests/test_update_shape_add_ignored.py: 170 passed;
  • test_path_guard_hygiene: 49 passed;
  • test_repo_hygiene: 101 passed;
  • test_no_auto_maintenance: 58 passed, 2 skipped.

Merges (git merge-tree --write-tree, open PRs re-listed at 00:08Z, 48 open): all clean against origin/main (4df3adec), #1166 (28c9bf88), #1133 (8289a9b9) and #1188 (dc71966f).

Sonar: gate OK, 0 new issues, 0.0% duplication on new code, no hotspots. CI on 7a9d397b: tests, tests-git-bash, package and SonarCloud all green, on attempt 1.

Body: a draft with no milestone and the follow-up sentence first. It ends Closes #1173, the Lane line, then the robot line, with one closing keyword, nothing above U+007F but the robot, and no bot block. Every other claim I checked holds at the head. The commit is by Brett Heap <service@opensoft.dev> and ends with Co-Authored-By.

Lane: openreposhape-3 (openRepoShape-3)

…THONUNBUFFERED` so it passes whoever runs it, and the refused-read row pins that the rollback is said once, under the refusal and not above it (#1173)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-23T00:23:54Z) Delta review (RV1187, opus) of #1187's fix round: head cc8a87ee, on 7a9d397b.

Verdict: READY. No new finding. Land only once CI's tests on cc8a87ee is green; it was still in progress at 00:23Z.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 4 hours and 9 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

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

Labels

None yet

Projects

None yet

1 participant