Skip to content

update-shape.py: the four functions Sonar scored at 24, 19, 16 and 47 for cognitive complexity hand each decision to a named helper, so every one of them is readable alone and every refusal, message and exit code is byte-identical (#133) - #139

Merged
brettheap merged 5 commits into
mainfrom
refactor/update-shape-cognitive-complexity
Sep 12, 2026

Conversation

@brettheap

@brettheap brettheap commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openreposhape-2 (openRepoShape-2)
Claim: #133 (comment)

Closes #133.

What was wrong

SonarCloud's python:S3776 named four functions in update-shape.py. The rule
counts +1 per branch, loop and boolean-operator sequence, plus one per level of
nesting, so a number this far over the limit of 15 is a statement about how many
decisions a reader has to hold at once. cmd_apply at 47 was the extreme case:
one body carrying every refusal, the rollback ledger, the copy, both pin
rewrites, the validator run and the branch/push/pull-request landing, so a human
who wanted to check one refusal read all of it.

What landed

Code moved, nothing changed. Every refusal id, message, remediation, print, exit
code and exception type is byte-identical, and no side effect was reordered.

function cc before (Sonar / complexipy) cc after how it was split
classify 24 / 24 1 the loop stays; each row goes to _classify_row (3), which keeps the three "cannot be compared" guards and hands the verdict ladder to _row_verdict (11)
upstream_copies 19 / 21 2 the parse and its two failure exits stay; _tuple_constants (9) is the ast scan, _listed_copies (10) the mapping of the three lists onto Copy
rewrite_manifest 16 / 15 7 _shape_block_bounds (8) answers where the shape: block is and raises update-manifest-no-shape; the rewrite stays
cmd_apply 47 / 59 8 one helper per phase (below)

cmd_apply's phases, in the order it still runs them: _refuse_unknown_accept_local
(1), _note_untaken_additions (4), _nothing_to_do (1), _refuse_conflicts (5),
_refuse_local_drift (4), _write_copies (10), _repin (0),
_refuse_red_validators (3), _land (3). Its two nested closures and the two undo
loops in the except Refusal arm became one Rollback object (write 0,
write_added 4, undo 8), which keeps the distinction they had: a file the root
already owned is restored from its first bytes, a file --add created is deleted
along with any directory made to hold it.

cmd_apply keeps its sequence, its try/except/finally and both return
values. Nothing moved across the boundary scripts/shape_materialize.py shares:
commit_on_branch and the --trailer path (#112) are untouched and only called.
cmd_check is deliberately out of scope, as the issue records — Sonar does not
flag it, and touching it would put a function at the limit into this diff for no
gain.

Pinned files touched: none. The diff is update-shape.py alone, 417
insertions and 222 deletions. No --help text moved, so docs/cli.md is
unchanged and tests/test_cli_reference.py still passes.

Follow-ups left, not fixed here: none — no bug was found while restructuring.

How it was proven

  • The proof the issue names ran first and green at every step:
    tests/test_update_shape.py, tests/test_update_shape_add.py and
    tests/test_commit_trailers.py, 178 passed. They assert the exact refusal ids
    (update-accept-local-unknown, update-conflict, update-local-drift,
    update-add-would-overwrite, update-validators-red, update-branch-required,
    update-manifest-no-shape), the exact NEXT spellings including
    --trailer "Lane: $LANES_LANE", the rollback-on-red-validator behaviour and the
    per-file --accept-local/--add semantics from update-shape.py: --accept-local re-pins a both row from the merged file, as AGENTS.md promised, and --branch may name the branch you are on (#84) #85 and update-shape.py: check reports upstream-added; apply --add <path> takes a file the standard gained since the pin #29.
  • Whole suite from the worktree, in two foreground chunks:
    tests/test_[a-m]*.py 366 passed, 1 skipped; tests/test_[n-z]*.py 770 passed.
    1136 passed, 1 skipped — the baseline on main today, with no new red and
    no new skip.
  • complexipy 8.0.1 (complexipy update-shape.py -d low) is the local measure
    behind every number in the table above; no function added by this pull request
    scores above 11.
  • The authority is this pull request's own SonarCloud analysis, queried
    PR-scoped after CI at head 4f302ec:
    curl -s 'https://sonarcloud.io/api/issues/search?componentKeys=opensoft_openRepoShape&pullRequest=139&statuses=OPEN&ps=100'
    returns "total": 0 — no python:S3776 left in this file, and no new
    issue of any rule the refactor could have introduced (S1192 duplicated
    literals, S1172 unused parameter, S3776 in a new helper). The SonarCloud
    gate passes, as do tests, tests-macos and tests-windows.
  • Copilot reviewed twice. Round 1 (head 184becb): approval recommended, three
    documentation findings — two docstrings overstating that _row_verdict can
    never be reached with a missing byte-string, when the PINNED blob may be
    None by design, and one sentence that compared against a score without
    naming it. All three taken in 4f302ec, which is docstrings only: stripping
    every docstring from both revisions and comparing ast.dump gives an
    identical tree. Round 2 (head 4f302ec): approval recommended, one naming
    nit (rename Rollback to _Rollback) declined on the record — this file
    names none of its six classes with a leading underscore, and the convention
    the refactor followed is the underscore for private functions, which all
    fourteen new helpers carry.

🤖 Generated with Claude Code

brettheap and others added 4 commits September 11, 2026 23:11
…and the verdict ladder to `_row_verdict`, so the function Sonar scored at 24 is a loop over rows and each decision it makes is readable alone (#133)

SonarCloud's `python:S3776` scored `classify` at 24 against a limit of 15
(complexipy 8.0.1 agrees: 24). One loop body answered three "these bytes
cannot be compared" questions and then walked a five-branch verdict ladder
over three derived booleans, so the four byte-strings the docstring names —
the recorded digest, the root's bytes, the upstream's bytes at the pinned
commit and at the target — had to be reconstructed from the ladder by anyone
reading it.

It is now three functions and the same code. `classify` is the loop.
`_classify_row` holds the three exits where a byte-string does not exist at
all (no file in the root, no upstream path this pin maps to, nothing at the
target), each of which is a human's decision rather than a difference this
tool acts on. `_row_verdict` is the ladder, reached only when all four read.

Nothing moved across the reads: the root's bytes, then the pinned blob, then
the target blob, in that order, because `Upstream.blob` shells out to git and
a reordering would change what a failing run prints. `file_sha256` is still
called exactly where it was, passed into `_row_verdict` as the argument that
was the local variable. Every state, detail string and digest assignment is
byte-identical, `continue` becoming `return` in a body that now ends the
function instead of the iteration.

classify 24 -> 1; _classify_row 3 (new); _row_verdict 11 (new).

Proven by tests/test_update_shape.py, tests/test_update_shape_add.py and
tests/test_commit_trailers.py: 178 passed.

Lane: openreposhape-2 (openRepoShape-2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…aterializer's copy lists are and `_listed_copies` what they mean, so the `ast` scan and the mapping onto `Copy` are two functions rather than one body Sonar scored at 19 (#133)

SonarCloud's `python:S3776` scored `upstream_copies` at 19 against a limit of
15 (complexipy 8.0.1 scored it 21). It did two unrelated jobs in one body: an
`ast` walk that harvests the upstream module's top-level string and tuple
constants, and then the mapping of the three copy lists that walk found onto
`Copy` objects. Reading either required stepping over the other.

The scan is now `_tuple_constants`, which returns only the tuples — the
strings exist so `as_literal` can resolve a list entry that names a constant
rather than repeating a path, and they are not the answer, so returning them
would have left the caller holding a value it never reads. The mapping is now
`_listed_copies`, which keeps both loops in their original order, so a
`..._FROM_SHAPE` entry still overwrites a verbatim entry of the same path
exactly as it did before. `upstream_copies` keeps the two exits that make it
the entry point: no materializer at that commit, and a module that will not
parse.

No shape check moved and none was relaxed: the entry-shape tests each loop
runs are the ones that were there, because these tuples arrive from a file
read out of somebody's git history and a copy list this tool cannot vouch for
is dropped rather than guessed at.

upstream_copies 21 -> 2; _tuple_constants 9 (new); _listed_copies 10 (new).
(complexipy's numbers; Sonar scored the original 19.)

Proven by tests/test_update_shape.py, tests/test_update_shape_add.py and
tests/test_commit_trailers.py: 178 passed. `tests/test_update_shape_add.py`
is the one that drives this path — it renames copy-list entries out of a
throwaway upstream clone and asserts the `upstream-added` verdicts that
come back.

Lane: openreposhape-2 (openRepoShape-2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the `shape:` block is and then only edits inside it, so finding the block and rewriting it are no longer interleaved in one body Sonar scored at 16 (#133)

SonarCloud's `python:S3776` scored `rewrite_manifest` at 16 against a limit of
15. Two scans located the block — one for the `shape:` key, one for the next
top-level key after it — with the `update-manifest-no-shape` refusal between
them, and only then did the rewrite begin. Finding the block is a different
question from editing it, and the answer is what SCOPES the edit: `commit:`
and `tree_sha256:` are spelled the same way in every pin a manifest carries,
so a rewrite that ran past the end of this block would move a leg's pin while
claiming to move the shape's.

`_shape_block_bounds` now answers it and raises the one refusal that belongs
to it, because a bounds function with no bounds to return has nothing else to
do. The end of the block is unchanged in every case: the next non-blank
unindented line, or the end of the file when `shape:` is the last block a
scaffold wrote. `rewrite_manifest` keeps the rewrite, the `seen` counts and
the `update-manifest-unrecognised` refusal, all byte-identical.

rewrite_manifest 15 -> 7; _shape_block_bounds 8 (new). (complexipy's numbers;
Sonar scored the original 16.)

Proven by tests/test_update_shape.py, tests/test_update_shape_add.py,
tests/test_commit_trailers.py and tests/test_family.py: 252 passed. The
family suite is in that list because a holder mirrors the same pin into
`family.yaml` through this function.

Lane: openreposhape-2 (openRepoShape-2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he rollback ledger to a `Rollback` object, so the body Sonar scored at 47 is the sequence of what `apply` does rather than every decision inside all of it (#133)

The function carried, in one body: the `--accept-local` unknown-path refusal,
the `--add` notes, the nothing-to-do exit, the `update-conflict` and
`update-local-drift` refusals, two nested closures that remembered every byte
written, the copy/kept/added print loops, both pin rewrites, the validator run
and its refusal, the branch/push/pull-request landing, and the two loops in the
`except Refusal` arm that put the tree back. A reader who wanted to check one
refusal read all of it, which is what a cognitive complexity of 47 is a
statement about.

What is here is the same code in the same order, moved:

  * `Rollback` is the two closures and the two undo loops as one object. It
    keeps the distinction they had — a file the root already owned is restored
    from its first bytes, a file `--add` created is deleted along with any
    directory made to hold it — and `undo` unwinds the creations in reverse so
    a directory is only considered after the file it was made for is gone.
  * `_refuse_unknown_accept_local`, `_refuse_conflicts`, `_refuse_local_drift`
    and `_refuse_red_validators` are one refusal each, asked in the order they
    were asked in, with every id, message and remediation byte-identical. The
    reasons those orderings are deliberate — a mistyped path reported as a
    mistyped path, `both` alone having an accepted exit — moved with them.
  * `_note_untaken_additions`, `_nothing_to_do`, `_write_copies`, `_repin` and
    `_land` are the report, the exit test, the write pass, the re-pin and the
    three optional acts after a clean rewrite.

`cmd_apply` keeps the sequence, the `try`/`except`/`finally` and the two
`return` values, so what it reads as now is the order in which `apply` does
things. Nothing moved across the boundary `scripts/shape_materialize.py`
shares: `commit_on_branch` and the `--trailer` path (#112) are untouched, and
`_land` only calls them. complexipy 8.0.1 scores the function at 8, and every
helper at 11 or less.

Lane: openreposhape-2 (openRepoShape-2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 12, 2026 03:39

@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.

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 2 days and 13 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sourcery-ai

sourcery-ai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors the four Sonar-flagged functions in update-shape.py into focused helpers and a reusable rollback ledger, reducing cognitive complexity while preserving behavior, side-effect order, diagnostics, and interfaces byte-for-byte.

Flow diagram for the refactored apply command

flowchart TD
    A[cmd_apply] --> B[_refuse_unknown_accept_local]
    B --> C[_note_untaken_additions]
    C --> D{_nothing_to_do}
    D -->|yes| E[return 0]
    D -->|no| F[_refuse_conflicts]
    F --> G[_refuse_local_drift]
    G --> H[_write_copies]
    H --> I[_repin]
    I --> J[_refuse_red_validators]
    J --> K[_land]
    K --> L[return 0]
    H -. Refusal .-> M[Rollback.undo]
    I -. Refusal .-> M
    J -. Refusal .-> M
Loading

File-Level Changes

Change Details Files
Split row classification into a loop-level dispatcher, comparison setup helper, and verdict ladder without changing outcomes.
  • Keep the three non-comparable-row refusals in _classify_row.
  • Move byte-state and verdict precedence logic into _row_verdict.
  • Preserve read order, row mutations, and all existing details.
update-shape.py
Decompose upstream copy extraction and manifest block discovery into focused helpers.
  • Separate AST tuple harvesting from conversion into validated Copy mappings.
  • Extract shape: block boundary detection while preserving the missing-shape refusal.
  • Leave parsing failures, filtering, and rewrite behavior unchanged.
update-shape.py
Refactor apply orchestration into named phases and centralize rollback state.
  • Add helpers for acceptance checks, additions, conflicts, drift, copying, re-pinning, validation, and landing.
  • Replace nested write closures and undo loops with the Rollback ledger.
  • Retain the original operation order, exception handling, output, exit codes, and optional commit/push/PR flow.
update-shape.py

Assessment against linked issues

Issue Objective Addressed Explanation
#133 Refactor classify, upstream_copies, and rewrite_manifest into named private helpers so each function's cognitive complexity is at or below Sonar's limit without changing behavior. ✅
#133 Decompose cmd_apply into helpers for its refusals, rollback ledger, write pass, re-pin, validator handling, and landing sequence, while preserving the original order, rollback behavior, messages, refusal identifiers, exit codes, side effects, and exception behavior. ✅
#133 Keep cmd_check and unrelated files untouched, and preserve the documented CLI and test-visible behavior byte-for-byte. ✅

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

Copilot AI 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.

🟢 Approval recommended

The only findings are minor documentation nits; behavior-preservation tests pass.

Pull request overview

Refactors update-shape.py into focused helpers to reduce cognitive complexity while preserving behavior, diagnostics, ordering, and rollback semantics.

Changes:

  • Split classification, copy discovery, manifest rewriting, and apply phases.
  • Centralized rollback tracking for overwritten and newly added files.
  • Preserved CLI behavior and validation flow.
File summaries
File Summary Findings
update-shape.py Complexity-reduction refactor with centralized rollback handling. Two nit comments request clarifying docstrings for missing pinned blobs and helper contracts (2 votes and 1 vote).
Review details

Suppressed comments (2)

update-shape.py:1230

  • This new docstring sentence is grammatically incomplete: it ends with “scored that function at;” and omits the score before continuing with “split out”. Please complete the sentence so the documentation is readable.
    Sonar scored that function at; split out for #133, with the same writes in

update-shape.py:460

  • The preceding explanation also implies that _row_verdict is never called with a missing byte-string, but upstream.blob(pinned, source) may return None and is deliberately handled by that helper as an upstream change. Distinguish these three early exits from the missing-pinned-blob case so the new helper contract is accurate.
    THE THREE EXITS HERE ARE THE ONES WHERE THE COMPARISON CANNOT BE MADE AT
    ALL, because one of the four byte-strings `classify` names does not
    exist: the root has no file at this path, no upstream path corresponds to
    it, or the upstream has none at the target commit. Each is a decision for
    a human rather than a difference this tool can act on, so `_row_verdict`
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread update-shape.py Outdated
… say that the PINNED blob is the one byte-string allowed to be absent, so neither claims a contract the code does not have (#133)

Copilot read the three new docstrings against the code on this branch and found
that two of them overstate what the split guarantees. It is right, and the fix
is the prose: nothing here changes a statement, and the file's AST is identical
to the previous commit's once docstrings are stripped from both.

  * `_classify_row` said its three early exits cover "one of the four
    byte-strings" being missing, and that `_row_verdict` is therefore "never
    reached holding a hole". Three of the four are covered; the PINNED blob is
    not. `upstream.blob(pinned, source)` returns `None` for a source the
    standard did not carry at the pinned commit, and that value is passed
    straight on. The docstring now says which three the exits are, and that
    the pinned blob is the deliberate exception.
  * `_row_verdict` said it is "reached only for a row whose four byte-strings
    all read", which is the same overstatement from the other side, and its
    very next line is `pinned_bytes is None or ...`. It now says what a `None`
    pinned blob MEANS to the ladder: `upstream_changed` true and
    `verbatim_at_pin` false, because a copy cannot be a verbatim copy of a
    blob that was never there.
  * `Rollback`'s docstring compared the moved code to "a third of what Sonar
    scored that function at" without naming the score, which read to a
    reviewer as a sentence with a number missing from it. It names the 47.

Lane: openreposhape-2 (openRepoShape-2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 12, 2026 14:34
@brettheap

Copy link
Copy Markdown
Contributor Author

Lane: openreposhape-2 (openRepoShape-2)

Both suppressed comments from the 2026-09-12T03:43:48Z review are taken, in 4f302ec, along with the inline one. All three were documentation findings and all three were right; the commit changes prose only.

Suppressed comment 1 — update-shape.py:1230, the Rollback docstring reads as a sentence with a number missing. Taken. The clause was "which is a third of what Sonar scored that function at", which is grammatical but asks the reader to go and find the score before the comparison means anything — and you read it as an omission, which is evidence enough that it did not carry. It now names the number: "together about a third of the 47 Sonar scored that function at."

Suppressed comment 2 — update-shape.py:460, _classify_row's contract is inaccurate about the missing pinned blob. Taken, and this is the more important of the two. The docstring said its three early exits cover "one of the four byte-strings" being absent and that _row_verdict is therefore "never reached holding a hole". Three of the four are covered — the root's file, the mapped source, the target blob. The PINNED blob is not: upstream.blob(pinned, source) returns None for a source the standard did not carry at the pinned commit, and that value is passed straight through to _row_verdict, which reads it as upstream_changed. The docstring now names the three exits as the three where there is nothing to compare, and names the pinned blob as the deliberate exception that is handed on as None. _row_verdict's own docstring — the inline comment, same defect from the other side — is fixed in the same commit and now says what a None pinned blob means to the ladder.

Proof that behaviour did not move. Stripping every docstring from both revisions and comparing ast.dump of the two trees returns identical — the commit is docstrings and nothing else. tests/test_update_shape.py, tests/test_update_shape_add.py, tests/test_commit_trailers.py, tests/test_repo_hygiene.py and tests/test_cli_reference.py: 239 passed. The whole suite was 1136 passed / 1 skipped at 184becb, the baseline on main.

@sonarqubecloud

Copy link
Copy Markdown

Copilot AI 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.

🟢 Approval recommended

The only finding is a non-blocking naming nit; behavior and rollback semantics are preserved.

Review details

Suppressed comments (1)

update-shape.py:1229

  • Rollback is a private implementation helper: it is only constructed by cmd_apply, alongside the private _... helpers introduced here. The refactor's documented convention is to give such helpers a leading underscore (as in scripts/repo_shape.py:178); please rename this class to _Rollback and update its type references so it is not exposed as a public module symbol.
class Rollback:
  • Files reviewed: 1/1 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@brettheap

Copy link
Copy Markdown
Contributor Author

Lane: openreposhape-2 (openRepoShape-2)

Answering the one suppressed comment from the 2026-09-12T14:37:39Z review (update-shape.py:1229, rename Rollback to _Rollback). Declined, with the reason.

The underscore convention this refactor followed is for private functions, and every helper it adds honours it: _classify_row, _row_verdict, _tuple_constants, _listed_copies, _shape_block_bounds, _refuse_unknown_accept_local, _refuse_conflicts, _refuse_local_drift, _refuse_red_validators, _note_untaken_additions, _nothing_to_do, _write_copies, _repin, _land. Classes in this file do not carry it, and never have:

$ grep -n '^class ' update-shape.py
144:class Kind:
234:class Upstream:
346:class Row:
587:class Copy:
706:class Addition(Row):
1229:class Rollback:

Row, Addition, Upstream and Kind are exactly as module-internal as Rollback is — nothing outside this file constructs any of them — so renaming only the class this pull request introduced would make it the odd one out in its own file rather than bring it into line with anything. scripts/shape_materialize.py names Materialized the same way.

scripts/repo_shape.py:178's _Rec is a real counter-example, but it is a different situation: repo_shape.py is a digest-pinned library that ships into every project and is imported by other tools, so the underscore there marks what an importer must not reach for. update-shape.py is an entry-point script with no importers to shield — grep -rn Rollback across the repository (templates and tests included) returns four hits, all four inside this file: the definition, two annotations and the single Rollback() in cmd_apply.

The rename is safe in the mechanical sense, and if the house decides classes should take the underscore too, the right change is all six at once in its own commit rather than one new one here — this pull request's law is that it moves code without changing it, and a single inconsistent rename is a change to argue for on its own terms.

Everything else in the review is agreed: behaviour and rollback semantics are preserved, which is what the docstring-only proof (identical ast.dump with docstrings stripped) and the 1136 passed / 1 skipped suite say. CI is green on ubuntu, macOS and Windows, and the PR-scoped SonarCloud query reports 0 open issues — no python:S3776 left in this file and no new issue of any rule.

@brettheap
brettheap merged commit aa5912e into main Sep 12, 2026
6 checks passed
@brettheap
brettheap deleted the refactor/update-shape-cognitive-complexity branch September 12, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants