Skip to content

repo_shape.py and adopt-project.py: a plain integer of more digits than this Python reads, or a decimal too large to be finite, is yaml-unparsable by its line, where it was a ValueError traceback or "the number inf", and _read_as says a float in digits with no exponent, where it said "the number 1e+300" (#1212) - #1249

Draft
brettheap wants to merge 2 commits into
fix/1114-pin-row-value-read-asfrom
fix/1212-overflow-and-long-int-said

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1212

This PR is stacked on #1198 (for #1114): its base is fix/1114-pin-row-value-read-as, at d38e4e36d, and it lands after #1198, which is open and not on main. Its rows read the pin row's wording #1198 puts in. The code itself applies to main without #1198 (see "Without #1198" below).

What

RV1198's NIT-1 and NIT-2 on #1198, both older than it.

scripts/repo_shape.py, _scalar: a plain number this reader cannot make is refused, by its line.

REFUSED yaml-unparsable: <plan>: line 63: a plain integer of 4301 digits is too long for this Python to read as a number
Remediation: unquoted, YAML reads this value as a number, and this reader cannot make one of it, so it refuses it rather than guess. Write it in double quotes if it is text, or as a number of fewer digits, and run the command again.
REFUSED yaml-unparsable: <plan>: line 63: a plain decimal of 401 digits is too large to read as a number: a 64-bit float cannot hold it, so it would be infinite

adopt-project.py, _read_as: a float is said in digits, with no exponent. repr writes 1e+300, 1e-05 and 1.2345678901234568e+16, and the plan's reader reads each of those as text. The digits are now repr's (the shortest that read back as that float), written out in full by decimal.Decimal, with .0 added where they hold no point. So 9 * 300 + .5 is "the number 1000...000.0" (a 1 and 300 zeros), 0.00001 is "the number 0.00001", and 12345678901234567.5 is "the number 12345678901234568.0". The plan's reader reads each of those back as the same float. An integer, a boolean and every other value are said as before. So is #1198's wording for a number the reader reads as itself: "records commit: as the number 7, which is not text".

The change is in scripts/repo_shape.py (an import, a constant, the two branches of _scalar, and comments and docstrings) and in adopt-project.py (an import and _read_as).

Choices #1212 left open

Without #1198

Measured on main 50869f384, in a worktree without #1198, with check:

  • a follow_ups: item of four hundred 1s and .5: "item 1 of follow_ups: is the number inf, not text";
  • the same of three hundred 9s and .5: "is the number 1e+300"; and 0.00001: "is the number 1e-05";
  • probe_extra: of 4301 digits: exit 1, the ValueError traceback;
  • a pin's commit: of four hundred 1s and .5: records `commit: inf` (main's repr in _pin_row_problem).

None of the lines this PR changes is a line #1198 changes. Its commit put onto main alone merges clean (git merge-tree --merge-base d38e4e36d origin/main HEAD, exit 0), so its code would mend all of the above on main alone. On this head, the follow-up reads "is the number 1000...000.0", and the other three are the reader's refusal. What needs #1198 is the rows file: it imports #1198's helpers from tests/test_adopt_pin_row_values.py, and its rows for a pin's commit: read #1198's wording. The lane's order puts this PR after #1198, so it is stacked.

Behaviour change

  • A plain integer of more digits than this Python reads (4300 by default), anywhere in any YAML file the reader reads: REFUSED yaml-unparsable with the line and a remedy, where it was a ValueError traceback.
  • A plain decimal too large to be finite: the same refusal, where it was read as inf.
  • _read_as says a float in digits: 0.00001, not 1e-05.
  • Unchanged: 4300 digits is still a number (plan-pin-malformed for a commit:), and a quoted value is text. A float that repr writes without an exponent (7.5, -0.0, 0.1) is said as before.

Rows

tests/test_plan_number_limits.py is new, ASCII only, with 35 rows. The reader's rows call load_yaml and parse_yaml in this process, and the _read_as rows load adopt-project.py as a module. The plan's rows edit a plan plan wrote and run check and execute --yes, with a stub gh first on PATH and --local-remote-dir. Each asserts that gh was asked nothing, that no remotes directory was made, and that there was no traceback. The rows that end in a finding are @needs_filter_repo, as #1198's are. The refusal rows run anywhere, because execute loads the plan before it asks for git filter-repo. The 21 rows that read or build an integer of thousands of digits set sys.set_int_max_str_digits(4300) and PYTHONINTMAXSTRDIGITS=4300, so a host that changed the limit runs them as CI does; the 14 that say a float do not need it. The file also collects at any limit: the one control that compares with the longest integer makes it inside its row, under the setting (see "Round 2").

Group Rows On the base On this head
The reader refuses a number it cannot make, by its line: seven integer forms (block value, negative, zeros, sequence item, flow sequence, flow mapping, nested) and four decimals 11 11 failed (7 ValueError, 4 did not raise) 11 passed
The refusal follows this Python's setting (640, and 5000) 1 1 failed (ValueError) 1 passed
_read_as says a float in digits: 1e300, the largest float, 1.2345678901234568e16, 1e-05, the smallest 5 5 failed (the exponent spelling) 5 passed
check and execute refuse by the line: a commit: of 4301 digits, a key outside pins: of 4301 digits, a commit: too large to be finite 3 3 failed (a traceback twice, "the number inf" once) 3 passed
A float commit: and a float follow_ups: item are said without an exponent 4 4 failed (the exponent spelling) 4 passed
Control: the reader reads as before (the longest integer, a quoted long integer, the largest finite decimal, a quoted decimal) 4 4 passed 4 passed
Control: _read_as says 7.5, 7.0, -0.0, 0.1 and -3.25 as repr does 5 5 passed 5 passed
Control: _read_as says an integer, a long integer and a boolean as before 1 1 passed 1 passed
Control: a commit: of 4300 digits is still the plan-pin-malformed finding 1 1 passed 1 passed
All 35 24 failed, 11 passed 35 passed

The base is #1198's head d38e4e36d merged with main bd3e9c148 (scratch commit d6ed89391, tree 8949c1b2d), with this file added. 80ec29177 is merged with main the same way (scratch commit 3f5b71b3e, tree a09d575c5). Both runs had git-filter-repo 2.47.0 on PATH, so no row skipped. The 11 that pass on the base are the controls: they pin what must not change. Every one of the 24 failures is for the reason in the table.

Mutants

At 80ec29177: 16 mutants of the two files, all killed. Each ran the whole rows file, and both files were restored and their sha256 checked after every run.

Mutant Failed, of 35
R1: int() with no guard 10
R2: the integer refusal without its remedy 10
R3: digits counted against a fixed 4300 1
R4: the sign counted as a digit 1
R5: no infinity check 5
R6: the decimal refusal without its remedy 5
R7: only positive infinity caught 1
R8: the point counted as a digit 5
R9: a long integer read as text 15
R10: an infinite decimal read as text 5
A1: repr put back in _read_as 9
A2: no .0 added 6
A3: format(x, "f") 14
A4: Decimal(x), the exact binary value 8
A5: .0 always added 8
A6: str(x) 9

These ran at 80ec29177, before main moved from 50869f384 to bd3e9c148. The three changes in between (#1180, #1109 and #1235) leave scripts/repo_shape.py byte-identical (git diff 50869f384 origin/main -- scripts/repo_shape.py is empty), and _read_as in adopt-project.py is byte-identical between 80ec29177 and the merge with bd3e9c148 (the same sha256 of the function). So five of the sixteen were run again on that merge, in a scratch worktree with each file restored and its sha256 checked: R1, R5, R7, A1 and A3 failed 10, 5, 1, 9 and 14 of 35, as in the table.

Gate

At 80ec29177: a scratch merge of it with main bd3e9c148: commit 3f5b71b3e, tree a09d575c5. It holds #1198's head d38e4e36d, its parent. Targeted files only, in the landing gate's environment: a stub gh first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, an empty global git config, GIT_ALLOW_PROTOCOL and GIT_SSH_COMMAND unset. git-filter-repo 2.47.0, CI's pin, was on PATH for every run below, so no row skipped for want of it. At most two pytest runs at a time, each with one reused basetemp.

The set is 35 test files, narrower than every file that names the reader's functions (grep -l for _scalar, parse_yaml, load_yaml or _read_as finds 134, and the full suite is CI's tests job on this PR):

Chunk Files Result
c1 the rows file, test_adopt_pin_row_values.py, test_adopt_pins.py, test_adopt_plan_value_order.py, test_adopt_plan_yaml_quoting.py, test_family_add_gitlink_and_null_members.py, test_leg_path_yaml_scalars.py, test_update_shape_header_comments.py, test_windows_paths.py, test_yaml_block_scalar_keys.py 1193 passed, 8 skipped
c2 test_yaml_flow_brackets.py, test_yaml_flow_quotes.py, test_yaml_indicator_openers.py, test_yaml_quote_boundaries.py, test_yaml_reader_spaces.py, test_yaml_flow_mappings.py, test_load_yaml_refusals.py, test_reader_refusals_named.py, test_refusal_default_remediation.py, test_adopt_plan_escapes.py 1066 passed
c3 test_adopt_execute_refusals.py, test_adopt_values_as_typed.py, test_adopt_manifest_values.py, test_neutral_pin_entry_one_spelling.py, test_scaffold_manifest_values.py, test_family_init_values.py, test_template_text_placeholders.py, test_repo_hygiene.py, test_path_guard_hygiene.py 1005 passed, 19 skipped
c4 the six source-reading files 579 passed
d0 to d3 test_shape_doctor.py, 112, 113, 112 and 112 ids 111 passed 1 skipped, 112 passed 1 skipped, 111 passed 1 skipped, 112 passed

Every chunk exited 0. In all, 4289 passed, 30 skipped, 0 failed. Every skip is one the files declare and none cites git filter-repo: 8 in c1 (test_leg_path_yaml_scalars.py: five empty parameter sets and three where the templates already read the value back), 19 in c3 (test_family_init_values.py: templates that quote the value), and 3 in the doctor shards (the PowerShell line is printed only on a Windows host).

The rows file ran by itself on the merge: 35 passed. With no git-filter-repo on PATH it is 30 passed and 5 skipped, which are the five rows that end in a finding.

Main then moved to b1f45fa76 (#1144, which edits shape-doctor.py, scripts/shape_advisory.py, tests/test_path_guard_hygiene.py and adds a test file). It touches neither file this PR changes. 80ec29177 merged with it (scratch commit 05f5e65c8, tree 3e3962f43) is clean, and on that merge the rows file, test_path_guard_hygiene.py, test_shape_advisory.py and test_repo_hygiene.py ran again: 304 passed.

Merge-trees

At 80ec29177: git merge-tree --write-tree against each, all clean (exit 0), at the heads fetched on 2026-11-08 (main at 14:01Z, #1169 and #1237 at their heads of 14:04Z):

None of the 24 touches tests/test_plan_number_limits.py. #1210 and #1125 add new _read_as callers: they benefit from the digits and do not conflict with them.

Round 2: RV1249's review, 3146c3ca5 on top of 80ec29177

RV1249 (#1249 (comment)) graded 80ec29177 NOT READY: no MUST, one SHOULD and four NITs, with the behaviour right. One more commit, 3146c3ca5, answers the SHOULD and two NITs, in tests/test_plan_number_limits.py and scripts/repo_shape.py and no other file. NIT-2 (_read_as of inf and nan, which the reader never makes) and NIT-3 (the helpers left alone still spell 1e-05, which pairs with str(value), the text execute writes) are left as RV1249 left them.

  • SHOULD-1, the rows file did not collect under a changed limit. MADE held int(LONGEST), 4300 digits, which was evaluated at import, so PYTHONINTMAXSTRDIGITS=1000 ended in "1 error, no tests collected" before the limit-setting fixture ran. integer-at-the-limit now holds None, and the row makes int(LONGEST) itself, under the fixture. The 35 rows pass with the variable unset, at 1000, at 640 and at 0 (on 80ec29177, at 1000: the collection error).
  • NIT-1, four composite assertions (Sonar S9073) at the old lines 212, 214, 321 and 338, are now one assertion for each fact (four become eight), the two on return codes with the output as their message.
  • NIT-4, "for this Python" was wrong for the decimal. Infinity is a limit of the 64-bit float, not of this Python. The refusal now reads a plain decimal of 401 digits is too large to read as a number: a 64-bit float cannot hold it, so it would be infinite. The line it names, the digit count and _UNMADE_NUMBER_REMEDY are as they were. The integer's refusal still says "for this Python", which is true of it (sys.set_int_max_str_digits sets that limit). Only too_large in the rows pinned the old words: README.md, AGENTS.md and docs/ do not hold them. Two comments in scripts/repo_shape.py that called the decimal a number "this Python cannot make" no longer do.
Check Result
The rows file, PYTHONINTMAXSTRDIGITS unset, 1000, 640, 0 35 passed each (git-filter-repo 2.47.0 on PATH, no skips)
The rows file on 80ec29177 at 1000 1 error, no tests collected
Mutants (RV1249's R1 to R14 and A1 to A11, and five of this round: the old wording put back, "64-bit" dropped, "integer" for "decimal", and the integer control compared with int(LONGEST) + 1 or never made) 30 of 30 killed, each file restored and its sha256 checked. R3, R4, R7 and R13 are each held by one row, as RV1249 measured
Gate, a scratch merge of 3146c3ca5 with main dffc42265 (commit 83698e6a3, tree eda7be55f; it holds #1198's head d38e4e36d): the rows file, #1198's three test files, both hygiene files, test_cli_reference.py, the reader's three refusal files, the six files that read the changed file's source, and test_shape_doctor.py in four shards of 449 ids 1597 passed, 3 skipped (the PowerShell line, Windows only), 0 failed
Main then moved to 5926cccab (adopt-project.py, docs/cli.md and a new test file). A second scratch merge of 3146c3ca5 with it (commit 41e904e86, tree 55fcaf8bb): the rows file (also at PYTHONINTMAXSTRDIGITS 1000 and 0), #1198's three test files, both hygiene files, test_cli_reference.py, the reader's three refusal files and the new test_adopt_plan_partial_clone.py 587 passed, 0 skipped, 0 failed
git merge-tree --write-tree of 3146c3ca5 against origin/main at both dffc42265 and 5926cccab, #1198's head, #1171 8162b874e and the 24 other open PRs that change adopt-project.py all clean; against #1198 the tree is 34d5c9541, this head's own

Not in this PR

🤖 Generated with Claude Code
Lane: openreposhape-4 (openRepoShape-4)

Summary by Sourcery

Make YAML number handling fail safely and describe floating-point values in forms the plan reader can preserve.

Bug Fixes:

  • Handle oversized plain integers and non-finite plain decimals as line-specific YAML parsing refusals instead of tracebacks or infinite values.
  • Render floating-point values in reader-compatible decimal notation without exponent forms.

Tests:

  • Add coverage for number limits, configurable integer digit thresholds, float rendering, and check/execute refusal behavior.

…an this Python reads, or a decimal too large to be finite, is `yaml-unparsable` by its line, where it was a `ValueError` traceback or "the number inf", and `_read_as` says a float in digits with no exponent, where it said "the number 1e+300" (#1212)

RV1198's NIT-1 and NIT-2 on #1198, both older than it; this follows #1114
and #1198.

- `scripts/repo_shape.py`'s `_scalar` catches the `ValueError` `int()`
  raises past `sys.get_int_max_str_digits()` (4300 by default), and refuses
  a decimal `float()` makes infinite. Each is a `YamlError` carrying a
  remedy, so `_block_value` records its line and `load_yaml` names it:
  `REFUSED yaml-unparsable: <file>: line 63: a plain integer of 4301 digits
  is too long for this Python to read as a number`. The words give the
  count of digits and not the value, which can be thousands of characters.
- `adopt-project.py`'s `_read_as` says a float in the digits of its `repr`
  written out in full, with `.0` where they hold no point, so the plan's
  reader reads them back as that float: "the number 0.00001", not
  "the number 1e-05". An integer, a boolean and every other value are said
  as before.
- `tests/test_plan_number_limits.py` is new: 35 rows, 24 of which fail on
  `d38e4e36d` and pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap brettheap added this to the v1.2.1 milestone Nov 8, 2026
@sourcery-ai

sourcery-ai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implementation

Refuse plain integers that exceed Python’s configured digit limit and decimals that overflow to infinity, carrying a remediation so the YAML reader reports the source line.

  • scripts/repo_shape.py L589-597, L613-622, L638-651 (+4 small edits)

Spell floats in the shortest decimal digits that round-trip through the plan reader, avoiding exponent notation and adding .0 where needed.

  • adopt-project.py L220, L8383-8395

Tests

Add reader, float-rendering, and end-to-end plan tests for number limits, line-specific refusals, and unchanged controls.

  • tests/test_plan_number_limits.py L1-350

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

RV1249: independent review of #1249 at 80ec2917: NOT READY

Independent review (RV1249, lane openRepoShape-4, 2026-11-08). Reviewed #1249's own delta, git diff d38e4e36d 80ec2917 (3 files: scripts/repo_shape.py, adopt-project.py, tests/test_plan_number_limits.py), against #1212's "Done when". Python 3.12.3 (the bench; CI's 3.14 is not available here), git-filter-repo 2.47.0 (a40bce548d2c) first on PATH for every run, gh stubbed to exit 97, GH_TOKEN=bogus, empty GH_CONFIG_DIR, empty global git config, GIT_SSH_COMMAND=false.

Verdict. The behaviour is right: no MUST. One SHOULD, in the rows file only: the body's claim that a host that changed the int limit runs the rows as CI does is false for a limit below 4300 (the whole file fails to collect). It is a 3-line fix, shown below. Three NITs.

Findings

SHOULD-1. tests/test_plan_number_limits.py does not collect on a host whose int-digit limit is below 4300, against the body's claim. The body says the 21 rows that build an integer of thousands of digits set the limit to 4300, "so a host that changed the limit runs them as CI does", and the file's own comment says the same of DEFAULT_DIGITS. But MADE (module level, line 147) evaluates int(LONGEST) at import, before any fixture runs:

"integer-at-the-limit": (f"k: {LONGEST}\n", int(LONGEST)),

Measured: PYTHONINTMAXSTRDIGITS=1000 pytest --co tests/test_plan_number_limits.py gives ERROR tests/test_plan_number_limits.py - ValueError: Exceeds the limit (1000 ...), no tests collected, 1 error: all 35 rows are lost, the 14 float rows that need no big integer included. At 0 and 5000 it collects 35. Fix: keep the expected value out of the module body, e.g. "integer-at-the-limit": (f"k: {LONGEST}\n", None) and, in test_a_number_the_reader_can_make_reads_as_it_did, if value is None: value = int(LONGEST) (after the fixture has set the limit). Verified on a scratch copy: with that change the file is 35 passed at PYTHONINTMAXSTRDIGITS=1000 and at 0 (file restored, sha256 checked). Grade note: if the lane judges a non-default host limit out of scope, this is a NIT; the PR claims it, so I graded it SHOULD.

NIT-1. Four open Sonar S9073 ("split this composite assertion") smells in the rows file, lines 212, 214, 321 and 338 (assert "e" not in digits and "inf" not in digits; assert isinstance(read, float) and repr(read) == repr(value), read; assert "e+" not in found and "e-0" not in found, found[:400]; assert checked.returncode == 1 and executed.returncode == 2). The quality gate passed with them, but they are real smells and the file is being edited for SHOULD-1 anyway: split each into two asserts.

NIT-2. _read_as of a non-finite float says "the number Infinity.0" / "the number NaN.0". format(Decimal(repr(inf)), "f") is Infinity, which has no point, so .0 is appended. Unreachable today: every caller is handed a value load_yaml made, and _scalar now refuses an infinite decimal and makes no NaN. If a future caller hands it JSON's Infinity, the words name a value no plan holds. Optional: a one-line math.isfinite guard (say repr for a non-finite) or a comment that the branch is for the reader's floats only.

NIT-3. Two spellings of one float across findings (the body's "Not in this PR", answered). The body says which commands reach _the_loader_made / shape_materialize._read_as / _reading_in_words with a float "was not run". Run (head, check on a plan with tracking_branch: 0.00001 and follow_ups: [0.00001]):

FINDING plan-bad-tracking-branch: `tracking_branch:` was read out of the plan as the number 1e-05, not as text, so `execute` would take it as the text `1e-05`
FINDING plan-bad-follow-ups: item 1 of `follow_ups:` is the number 0.00001, not text: ...

So one typed 0.00001 is "1e-05" in one finding and "0.00001" in the next. It does not matter to a user in the first: that sentence pairs the number with str(value), the text execute writes (1e-05), so it is accurate as it stands, and changing _the_loader_made would make it wrong. Leave it; a follow-up could say "(written 1e-05)" or unify, not a blocker. The same helpers cannot be handed inf any more (checked).

NIT-4. Wording only. The decimal refusal says "too large for this Python to read as a number". An infinite decimal is a limit of an IEEE double, not of this Python (the integer refusal is Python's). "too large to be a finite number" would be exact.

What I measured

1. Pinned copies. No copy, fixture or digest of repo_shape.py is stored in the repo (git ls-files | grep repo_shape is the one file; no 64-hex digest names it). The copies are written at scaffold/adopt/family init time from scripts/repo_shape.py through shape_materialize.py's copy lists (REPO_SHAPE, lines 440 and 649: the assembly root's and the family holder's), and contracts/shape-pin.yaml digests them then. So the change reaches every root as upstream-changed scripts/repo_shape.py on the next update-shape.py check, as #1126's change to the same file does (main has it); nothing is out of step. No name is added or removed (one import math, one private constant), so the #536/#604 update-accept-local-missing-name cases do not arise. #370/#610: I put v1.1.1's validate-manifest.py, validate-pins.py, bootstrap.py and validate-repository-naming.py beside the head's repo_shape.py in a scratch git root whose project.yaml holds schema_version: of 4301 digits (limit 4300): both validators exit with REFUSED yaml-unparsable: ...project.yaml: line 1: a plain integer of 4301 digits is too long ... and the remedy; the same root with d38e4e36d's repo_shape.py ends in ValueError: Exceeds the limit (4300 digits). So an existing root's pinned validators meet nothing they cannot run, and the change is an improvement for them once re-pinned. Copy-comparison tests all passed: test_pinned_copy_text.py, test_update_shape_kept_helper.py, test_bootstrap_spelling.py, test_bootstrap_pasteable_lines.py, test_doctor_validator_rows.py, test_doctor_validator_site.py, test_openreposhape_package.py, test_release_workflow.py, both hygiene files (counts below).

2. _scalar behaviour (own probes on the head; plus a differential of base d38e4e36d against head over 975 generated scalars):

  • Integers: 4299 and 4300 digits are ints; 4301 and 4302 refuse (the sign is not counted); - + 4300 digits is an int, - + 4301 refuses; 4301 zeros refuse (CPython counts leading zeros, so does the message); + + 4301 digits stays text (as on base).
  • sys.set_int_max_str_digits: the code never touches it. At 0: 20000 digits read as an int. At 640: 641 refuses, 640 reads. At 5000: 5001 refuses, 5000 reads. At 100000 all read.
  • Decimals: 308 digits + .5 is finite (1e+308); 309 digits + .5 refuses ("decimal of 310 digits"); 400 ones + .5 and its - form refuse; 4400 digits + .5 refuses; 1. + 5000 fraction digits reads (finite); 0. + 400 zeros + 1 reads as 0.0 (underflow, not refused, as the body says); 1.7976931348623157e308 and 1e309 / -1e309 / 1e-400 / 1E5 stay text (no exponent form is read, as on base).
  • inf, -inf, +inf, Inf, INF, .inf, -.inf, .Inf, nan, NaN, .nan, .NaN, -.nan, infinity, 0x1F, 1_000, 1:30: all text, unchanged.
  • Quoted (double, single) 4301-digit and 400-digit values stay strings; block and folded scalars stay strings; a 4301-digit KEY stays a key.
  • Differential over 975 inputs (lengths 1-5000, signs, zeros, nines, decimals, Arabic-Indic digits): the ONLY accepted-before-refused-now values are the 179 decimals that read as inf or -inf; the only other change is 27 ValueError becoming YamlError. No other value changes.
  • load_yaml names the right line for both refusals in 16 file shapes (first and second dash keys, - - x, after a block scalar, CRLF, BOM, comment lines, trailing space, tab after the colon, 4-deep mapping, flow mapping, flow sequence, nested flow, negative, quoted key beside a big value, with a trailing comment) and in all the rows' positions; every refusal carries the remedy; the message holds the digit count, never the value.
  • Callers: the parse_yaml round-trip helpers (_reads_back, _reading_of, family.py's id check, adopt-project.py's _reads_back_as_text) catch YamlError, so a long all-digit text is now quoted, not a traceback; update-shape.py's and the doctor's pin readers catch it too.

3. _read_as floats. 31 hand-picked floats (0.0, -0.0, 1e15, 1e16, 1e22, 1e-05, 5e-324, the largest, 1.2345678901234568e17, 0.30000000000000004, ...) and 200000 random bit-pattern floats (seed 1249): no exponent in any, a point in every one, .0 only where repr's digits hold no point (7.0 stays "7.0", 1e+16 is "10000000000000000.0"), -0.0 keeps its sign, and every one reads back through parse_yaml as the same float (sign of zero too). Longest outputs: 326 characters for 5e-324, 303 for 1e300, which is what #1212 asked for (no exponent). Decimal(repr(x)) is exact whatever the decimal context. Int, bool, text, null, list and mapping wording unchanged.

4. Rows. On d38e4e36d merged with main (105b5daf9) plus the new file: 24 failed, 11 passed, exactly the body's. Failure causes by line: 7 ValueError at _scalar, 1 ValueError at the 640 setting, 4 "DID NOT RAISE Refusal" (decimals), 3 check/execute rows (2 with a Traceback, 1 records commit: as the number inf), 5 _read_as exponent spellings, 4 float finding wordings = 24. On the head merged with main: 35 passed (and 80 passed with #1198's 45). With no git-filter-repo on PATH: 30 passed, 5 skipped (the five @needs_filter_repo rows, skip reason names it, the refusal rows run).

5. Mutants (25, on a scratch merge of head and main; whole rows file each; both files restored and sha256 checked after every run, restored=True for all). All killed:

Mutant Failed of 35
R1 int() with no guard 10
R2 integer refusal without remedy 10
R3 digits counted against a fixed 4300 1
R4 the sign counted as a digit 1
R5 no infinity check 5
R6 decimal refusal without remedy 5
R7 only +inf caught 1
R8 the point counted as a digit 5
R9 isnan in place of isinf 5
R10 the long integer read as text 15
R11 a plain ValueError raised, not YamlError 10
R12 remedy text changed 14
R13 negative infinity not refused 1
R14 integer message says "decimal" 10
A1 repr put back for a float 9
A2 no .0 added 6
A3 format(x, "f") 14
A4 Decimal(x), the binary value 8
A5 .0 always added 8
A6 str(x) 9
A7 abs() drops the sign 2
A8 whole-number formatting 11
A9 a float said as text 14
A10 .0 tested on repr, not on the digits 6
A11 exponent kept from 1e16 up 6

R3, R4, R7 and R13 are held by one row each (the setting row, integer-negative, decimal-negative). Dropping from exc was not run: nothing reads the YamlError's __cause__ (the rows assert the Refusal's).

6. Gate (scratch merge 0b671d663 = 80ec2917 + origin/main 105b5daf9; tree a0b45e886; 129 files, 6 chunks, then test_shape_doctor.py as 4 shards of 449 ids read from files into arrays; at most 3 pytest runs at a time; one reused basetemp per chunk): the rows file, #1198's three test files plus test_adopt_pin_row_values.py, all 122 files that name _scalar, load_yaml or _read_as, both hygiene files, test_cli_reference.py, test_scaffold_leg_path_collisions.py, test_adopt_template_values.py, and the copy-comparison tests of item 1.

Chunk Result
files 1 1431 passed, 5 skipped
files 2 1628 passed
files 3 1612 passed, 23 skipped
files 4 (holds the rows file) 1442 passed, 1 skipped
files 5 2367 passed, 8 skipped
files 6 1560 passed
doctor shards 1-4 112+1, 111+1, 112, 111+1 = 446 passed, 3 skipped (449 ids)

10486 passed, 40 skipped, 0 failed, every chunk exit=0. No skip cites git-filter-repo (grep -i 'filter.repo' over every log is empty; every skip is a declared one: reftable needs git 2.45, case-insensitive disk, NTFS junctions, SHAPE_MEDXEHR_CLONE unset, templates that already quote, empty parameter sets, Windows-only PowerShell line). Main then moved to dffc42265 (four commits, none touching repo_shape.py, adopt-project.py or the rows' imports, but one rewrote update-shape.py and test_update_shape_kept_helper.py); on a second scratch merge with it (732a57cb3), test_update_shape_kept_helper.py, test_neutral_pin_entry_one_spelling.py and test_pinned_copy_text.py gave 399 passed, and the rows file, test_adopt_pin_row_values.py, both hygiene files, test_cli_reference.py, test_bump_leg_reachability_words.py and test_update_shape_landing.py gave 452 passed, no skips.

7. Merge-trees (git merge-tree --write-tree, head 80ec2917): origin/main 105b5daf9 clean (tree a0b45e886; it differs from main in six files: adopt-project.py, scripts/repo_shape.py, #1198's three test files and the rows file, i.e. the stack's own); origin/main dffc42265 clean; #1198's head d38e4e36d clean (tree 89a4e55c0, the head's own); the delta alone onto main, git merge-tree --merge-base d38e4e36d origin/main 80ec2917, clean and touching exactly its three files (the body's "Without #1198" claim holds structurally); and clean with the heads of the 25 other open PRs that change adopt-project.py or repo_shape.py: #1105 b28f40de0, #1106 504a1dd65, #1118 2307036ab, #1120 c55e55e33, #1122 c01c4d73d, #1123 5c3908f8d, #1125 9549006fe, #1141 0b8fb3205, #1143 7af5fabc3, #1153 ff33ba12a, #1154 8a7828332, #1170 24ee133aa, #1171 8162b874e, #1177 5208e874e, #1178 c4beba193, #1186 07486a498, #1189 7d2f651e7, #1190 e42134e10, #1194 900f77af9, #1207 6fa1edd56, #1210 791106c2b, #1225 73c9f57e8, #1237 63c527185, #1251 825923c76, #1252 40b030cbf (#1141 and #1189 moved since the body's list; #1251 and #1252 are new). #1171 is the only other PR that edits repo_shape.py (adds code_point_escaped, DEFAULT_IGNORABLE*, visible_repr and a checked_value message; no overlap with _scalar): on a three-way scratch merge of the head, main and #1171, the rows file, #1171's three test files, test_pinned_copy_text.py, both hygiene files, test_adopt_pin_row_values.py, test_load_yaml_refusals.py and test_reader_refusals_named.py gave 656 passed. In #1125, #1141, #1171, #1189, #1210, #1251 and #1252 (grepped) no test pins an exponent spelling of a float.

Scratch: /workspace/projects/openRepoShape-worktrees/_lane4-scratch/tools/run/rv1249/ (probes, logs, mutate.py, mutants.out, chunk lists), clone rv1249-clone and worktrees rv1249-merge-wt, rv1249-merge2-wt, rv1249-base-wt, rv1249-mut-wt, rv1249-m1171-wt beside it. Nothing pushed or edited.

Lane: openreposhape-4 (openRepoShape-4)

…collect and run under any PYTHONINTMAXSTRDIGITS, each assertion states one fact, and the refusal of a decimal says a 64-bit float cannot hold it, where it said "for this Python" (#1212)

RV1249's review of #1249 at 80ec291 (see #1212), one SHOULD and two NITs
taken here:

- SHOULD-1: the rows file evaluated int(LONGEST), 4300 digits, at import, so
  a host whose limit is below that (PYTHONINTMAXSTRDIGITS=1000) could not
  collect the file at all, before the limit-setting fixture ran. The
  "integer-at-the-limit" control now holds None and makes the integer inside
  the row, under the fixture. The file passes 35 of 35 with the variable
  unset, at 1000, at 640 and at 0.
- NIT-1: the four composite assertions (Sonar S9073) are one assertion for
  each fact.
- NIT-4: the infinite decimal's refusal said "too large for this Python to
  read as a number". Infinity is a limit of a 64-bit float, not of this
  Python, so it now says "too large to read as a number: a 64-bit float
  cannot hold it, so it would be infinite". The line, the digit count and
  _UNMADE_NUMBER_REMEDY are as they were. Nothing else pins the old words:
  README.md, AGENTS.md and docs/ do not hold them. The integer refusal still
  says "for this Python", which is true of it, since sys.set_int_max_str_digits
  sets that limit. The two comments that called the decimal a number "this
  Python cannot make" no longer do.

NIT-2 and NIT-3 are left as RV1249 left them.

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

Copy link
Copy Markdown
Contributor Author

F1249 answers RV1249's NOT READY (#1249 (comment)). New head: 3146c3ca5f102624d0c36761ca192cbd7f60df93, one commit on top of 80ec29177, pushed without force. It touches two files, tests/test_plan_number_limits.py and scripts/repo_shape.py. #1198's branch is not touched.

Please give it a DELTA review of 3146c3ca5 only (git diff 80ec29177 3146c3ca5, 29 insertions and 17 deletions in the two files).

SHOULD-1, the rows file did not collect under a changed limit. MADE held int(LONGEST), evaluated at import. Now integer-at-the-limit holds None and the row makes the integer itself, under the limit-setting fixture. The 35 rows pass with PYTHONINTMAXSTRDIGITS unset, at 1000, at 640 and at 0 (git-filter-repo 2.47.0 on PATH, no skips). On 80ec29177 at 1000: "1 error, no tests collected". Also 35 passed at 1000 and at 0 on the merge with the current main.

NIT-1, Sonar S9073. The four composite assertions (old lines 212, 214, 321 and 338) are one assertion for each fact, eight in all; the two on return codes carry the output as their message.

NIT-4, "for this Python". The decimal refusal now reads a plain decimal of 401 digits is too large to read as a number: a 64-bit float cannot hold it, so it would be infinite. The line it names, the digit count and _UNMADE_NUMBER_REMEDY are as they were. The integer refusal keeps "for this Python", which is true of it. Only too_large in the rows pinned the old words: grep of tests/, README.md, AGENTS.md and docs/ for the old wording finds nothing else. Two comments that called the decimal a number "this Python cannot make" are reworded to match. The PR body's example line, one heading and the "At 80ec29177" anchors are updated, and it has a new "Round 2" section; I read it back.

NIT-2 and NIT-3 are left alone, as you graded them.

Mutants. 30 of 30 killed, each file restored and its sha256 checked (restored=True every time): your R1 to R14 and A1 to A11, and five of this round (the old wording put back: 5 failed; "64-bit" dropped: 5; "integer" for "decimal": 5; the integer control compared with int(LONGEST) + 1: 1; the control never made: 1). R3, R4, R7 and R13 each fail exactly one row, as you measured: R3 test_the_refusal_follows_this_pythons_setting, R4 [integer-negative], R7 and R13 [decimal-negative].

Gate (a scratch merge of 3146c3ca5 with main dffc42265, commit 83698e6a3, tree eda7be55f, holding #1198's head d38e4e36d; at most 2 runs at once, -o tmp_path_retention_policy=failed): the rows file, #1198's three test files, both hygiene files, test_cli_reference.py, the three reader refusal files, the six files that read the changed file's source, and test_shape_doctor.py in four shards of 449 ids. 1597 passed, 3 skipped (the PowerShell line, Windows only), 0 failed. Main then moved to 5926cccab; on a second merge (commit 41e904e86, tree 55fcaf8bb) the rows file, #1198's three files, both hygiene files, test_cli_reference.py, the three refusal files and the new test_adopt_plan_partial_clone.py gave 587 passed, 0 skipped, 0 failed. A wider batch of 56 files was started and stopped at 269 tests (0 failed) because the host was loaded; it is not part of the gate.

Merge-trees (git merge-tree --write-tree), all clean: origin/main at dffc42265 and 5926cccab, #1198's head (tree 34d5c9541, this head's own), #1171 8162b874e, and the 24 other open PRs that change adopt-project.py.

Lane: openreposhape-4 (openRepoShape-4)

@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

RD1249: independent delta review of #1249 at 3146c3ca: READY

Independent DELTA review (RD1249, lane openRepoShape-4, 2026-11-08) of git diff 80ec2917 3146c3ca only: 2 files, 29 insertions and 17 deletions (scripts/repo_shape.py, tests/test_plan_number_limits.py). The rest of #1249 was reviewed by RV1249 and is not re-reviewed here. Python 3.12.3 (the bench), git-filter-repo 2.47.0 first on PATH (no row skipped), gh stubbed to exit 97, GH_TOKEN=bogus, empty GH_CONFIG_DIR, empty global git config, GIT_SSH_COMMAND=false, basetemps under /tmp/lane4-rd1249-bt/.

Verdict. No MUST, no SHOULD. One NIT. All three RV1249 findings in scope (SHOULD-1, NIT-1, NIT-4) are answered.

What I measured

1. SHOULD-1 is fixed. The rows file on 3146c3ca, -rs, 35 passed and no skip each time: PYTHONINTMAXSTRDIGITS unset, 1000, 640 and 0. On the scratch merge with current main: 35 passed at 1000, at 0 and unset. On 80ec2917 at 1000: tests/test_plan_number_limits.py:147: ValueError: Exceeds the limit (1000 digits) for integer string conversion, "1 error in 0.58s", nothing collected (the old failure, reproduced).

2. The integer-at-the-limit row tests what it did. MADE["integer-at-the-limit"] holds None, and the row sets value = int(LONGEST) under the default_digits fixture before it compares parse_yaml(text) == {"k": value}. None is only the expected side, so it cannot excuse a broken reader. Mutants (all killed, see the table): a reader that returns None for every integer (S8) or only for exactly 4300 digits (S12) fails [integer-at-the-limit]; int(text) + 1 (S9), a refusal at 4300 (S10) and the integer read as text (S11) fail it too; and the row itself with the sentinel left in place (T1), compared with int(LONGEST) + 1 (T2) or with the text (T3) fails exactly that one row.

3. NIT-1 (S9073): nothing is dropped. The four composites are now eight asserts, each fact kept: "e" not in digits and "inf" not in digits; isinstance(read, float) and repr(read) == repr(value), each with the old message read; "e+" not in found and "e-0" not in found, each with found[:400]; checked.returncode == 1 and executed.returncode == 2, now each with its stderr as the message (checked.stderr[:2000] is already used a few lines above, and executed.stderr is already read as text below). An AST scan of the file finds no assert with a boolean operator left (32 asserts). SonarCloud at this head: quality gate pass, and the MCP search for OPEN or CONFIRMED issues on PR 1249 returns 0.

4. NIT-4: the wording is true, and nothing else pins the old text. The decimal refusal reads a plain decimal of N digits is too large to read as a number: a 64-bit float cannot hold it, so it would be infinite. Probed on the head: 1 + 309 zeros + .0 and its - form give N = 311; 400 ones + .5 gives 401; str(2**1024 - 2**970) + ".0" (the first value that rounds to infinity) is refused, and the largest finite float and a value above it that still rounds to it (17976931348623158 + 292 zeros + .0) are read as 1.7976931348623157e+308, so only a value a 64-bit float cannot hold is refused. The integer refusal still reads a plain integer of 4301 digits is too long for this Python to read as a number, which is true: int() raises past sys.get_int_max_str_digits(), a setting of the interpreter. git grep on the merge with origin/main be21751c3 (README.md, AGENTS.md, docs/, templates/, every test): the only lines holding too large, digits is too, plain decimal, plain integer of or cannot hold it for this message are scripts/repo_shape.py and tests/test_plan_number_limits.py; the old words (too large for this Python, read as a number: it would be infinite, THIS PYTHON CANNOT MAKE) appear nowhere. The reworded header comment (THIS READER CANNOT MAKE) and the _scalar docstring now agree with the message.

Mutants (17, on 3146c3ca, whole rows file each; both files restored and sha256 checked after every one, restored=True for all, git status clean at the end)

Mutant Failed rows, of 35
S1 decimal says "for this Python" again 5
S2 "64-bit" dropped 5
S3 decimal message says "integer" 5
S4 decimal digit count off by one (point counted) 5
S5 decimal refusal without its remedy 5
S6 integer message loses "for this Python" 10
S7 decimal tail "so it is infinite" 5
S8 reader returns None for an integer 9 failed, 8 errors
S9 reader returns int(text) + 1 2 failed ([integer-at-the-limit], the setting row), 8 errors
S10 reader refuses an integer of exactly 4300 digits 3 ([integer-at-the-limit], the setting row, test_a_commit_of_4300_digits_is_still_a_finding)
S11 reader reads the integer as text 9 failed, 8 errors
S12 reader returns None only for exactly 4300 digits 2 ([integer-at-the-limit], test_a_commit_of_4300_digits_is_still_a_finding)
T1 row keeps the None (the sentinel is not replaced) 1 ([integer-at-the-limit])
T2 row compares with int(LONGEST) + 1 1 ([integer-at-the-limit])
T3 row compares with the text LONGEST 1 ([integer-at-the-limit])
T4 checked.returncode == 1 becomes == 2 1 (test_a_float_follow_up_is_said_without_an_exponent)
T5 executed.returncode == 2 becomes == 1 1 (test_a_float_follow_up_is_said_without_an_exponent)

Gate

On a scratch merge of 3146c3ca (which holds #1198's head d38e4e36d) with the CURRENT origin/main be21751c3: commit e9e9f2e09, tree 6248166af. It differs from main in exactly six files: adopt-project.py, scripts/repo_shape.py, tests/test_adopt_pin_row_values.py, tests/test_adopt_pins.py, tests/test_adopt_plan_value_order.py, tests/test_plan_number_limits.py. At most 2 runs at once on a host at load 40 to 65, -o tmp_path_retention_policy=failed, -rfs:

  • the rows file, test_adopt_pin_row_values.py, test_adopt_pins.py, test_adopt_plan_value_order.py, test_load_yaml_refusals.py, test_reader_refusals_named.py and test_refusal_default_remediation.py: 327 passed, 0 skipped, 0 failed (7m25s);
  • test_repo_hygiene.py, test_path_guard_hygiene.py and test_cli_reference.py: 245 passed, 0 skipped, 0 failed.

Total 572 passed, 0 skipped, 0 failed. (F1249's gate ran on main dffc42265 and then 5926cccab; main has moved to be21751c3, and the result holds on it.)

Merge-trees (git merge-tree --write-tree, head 3146c3ca)

Findings

NIT-1. MADE uses None as the "make it in the row" marker, and None is also what YAML null reads as. Harmless today: the only row with None is integer-at-the-limit, and a mis-set marker fails a row rather than passing one (T1 and S8 show it). But a later control row that wants {"k": None} (k: null) would be compared with int(LONGEST) and fail for a reason that is hard to see. Optional: a module-level MADE_BY_THE_ROW = object() sentinel, or a lambda: int(LONGEST) that the test calls. Follow-up only; it does not block.

Outside the delta (already noted by F1249, no action): the PR body's own mutant table numbers R9 to R13 differently from RV1249's.

Lane: openreposhape-4 (openRepoShape-4)

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant