Skip to content

scripts/repo_shape.py: a block scalar's indentation digit (|2, >-2) is refused by the header's line, and +, a deeper line of spaces and a tab line are read as YAML reads them or refused by their lines (#1004) - #1021

Merged
brettheap merged 4 commits into
mainfrom
l3-1004-block-scalar-indicators
Oct 20, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1004.

scripts/repo_shape.py's YAML reader now refuses a block scalar's indentation digit by the header's line. It reads the rest of a block scalar as YAML reads it, or refuses it by its line where PyYAML refuses the document. One commit, three files: the reader and two of its test files. No other file holds a copy of the reader: git grep finds one def parse_yaml, and no template script carries one.

The issue has no "Needs a ruling" or "Documented limit" section, so every section is done here.

What changed, by section, and why

RV974's SHOULD-1: the indentation digit is refused

The digit was accepted and never read. It is now refused by the header's line, before the body is read:

line 1: a block scalar's indentation indicator is not supported: '|2'

The words follow the reader's other refusals of what it does not take (explicit keys are not supported, merge keys are not supported). There is no remediation, as with #974's refusals: load_yaml gives yaml-unparsable with an empty remedy.

Refused, not read. The issue left the choice open. I took the Direction's smaller option, for three reasons:

Every header the old pattern took is still a header. _BLOCK_HEADER_RE is [|>](?:[-+]?\d*|\d+[-+]). Each header that holds a digit is refused by the one rule, _refuse_indentation_indicator. That covers:

  • |2, |1, >-2 and |+2;
  • |0, |10 and a digit of another script (U+0662), which the old pattern took and PyYAML refuses;
  • the digit-first |2-, which YAML reads. It was no header before: alone it read as the text '|2-', and over a body it was refused as unexpected indentation on the body's first line.

#974's row (k: |2 over a and b) is now refused at line 1, for its digit, and its test test_an_indentation_digit_is_still_not_read_and_now_refuses became test_an_indentation_digit_is_refused_by_the_headers_line. Before, it was refused at line 3, for the shallower line. As the issue said it would, the |2 rows of tests/test_yaml_block_scalar_keys.py's plain-twin test become refusals. They move to a test of their own, test_a_block_key_over_an_indentation_digit_is_refused_by_its_line, over |2, >-2 and |2+. The plain-twin test now runs over |, >, |-, >+, |+ and >-.

Document (after k:) main now PyYAML 6.0.3
|2 over a, b 'a\nb\n' refused, line 1 ' a\n b\n'
|1 over a, b 'a\nb\n' refused, line 1 ' a\n b\n'
>-2 over a, b 'a b\n' refused, line 1 ' a\n b'
|2- over a, b refused, line 2 refused, line 1 ' a\n b'
|0, |10 over a 'a\n' refused, line 1 refuses

RV974's second NIT: the four layouts, read as YAML reads them or refused

_block_scalar_body now reads a line of spaces and tabs by where its characters stand. Before, any such line was a blank line, whatever it held.

  1. + is honoured (_block_scalar_text). The empty lines at the block's end, and the line break after its last line of text, belong to the chomping indicator:

    • - drops them all;
    • no indicator keeps the one line break (YAML's clip);
    • + keeps it and every empty line after it.

    I chose to read + and not refuse it: refusing it would refuse a header YAML reads, and keeping it costs one line once the empty lines are counted.

  2. A line of spaces deeper than the block is text. The spaces past the block's column are kept, as for any deeper line.

  3. A blank line above the first line of text, and deeper than it, is refused by its line, as PyYAML refuses the document. The words name the first such line:

    line 2: a blank line above a block scalar's first line of text holds 6 spaces, more than the 2 of that line
    

    Decided beyond the issue: where that first line opens #, PyYAML reads it as a comment that ends the block, with no refusal (k: | over , # c, z: 1 is {'k': '', 'z': 1}). This reader does the same, by scripts/repo_shape.py: the YAML reader refuses a key written twice in one mapping, block or flow, by the line of the second, and a block scalar line indented less than its first by its line, where it kept the last key and cut the line's first characters; such a line opening # ends the block as a comment (#917) #974's comment rule.

  4. A tab left of the block's column, in a line that holds nothing else, is refused by its line. The words are the ones a tab in any other line's indentation gets: line 3: tab used for indentation.

Row Document (after k:) main now PyYAML 6.0.3
1 |+ over a, an empty line 'a\n' 'a\n\n' 'a\n\n'
1 |+ over a, two empty lines, then z: 1 'a\n' 'a\n\n\n' 'a\n\n\n'
2 | over a, , b 'a\n\nb\n' 'a\n \nb\n' 'a\n \nb\n'
3 | over , a '\na\n' refused, line 2 refuses
4a | over a, a tab, b 'a\n\nb\n' refused, line 3 refuses
4b | over a, a space and a tab, b 'a\n\nb\n' refused, line 3 refuses
tab at the column | over a, two spaces and a tab, b 'a\n\nb\n' 'a\n\t\nb\n' 'a\n\t\nb\n'

What the same reading changes, beyond the issue's rows

The issue's Size section said a YAML reading of + and of the deeper line of spaces would make _block_scalar_text and _fold_block_body carry the kept lines. Doing that changes three more readings, each to PyYAML's:

  • A folded block keeps the line breaks around a line that opens with a space or a tab (_fold_block_body). A deeper line of spaces in a > block is such a line, so row 2 cannot be read as YAML reads it in a folded block without this rule. With the rule, a deeper line of text is read the same way. Each was joined with a space: k: > over a, b, c read 'a b c\n', and now reads PyYAML's 'a\n b\nc\n'.
  • A block with no line of text is '' under clip, and its empty lines under +. It read '\n' (k: |, then z: 1). Under - it already read ''.
  • A tab-holding blank line on the block's first line is text, because its spaces set the column: '\t\na\n' where main read '\na\n'.

Checked by a differential fuzz against PyYAML 6.0.3 (random.Random seeds 1004 and 7). Each run makes block scalars under every header but the digit ones, at the top, nested, and in a sequence item. Their bodies hold up to five lines, each at a column from 0 to five past the key: empty, a, b, #c, a tab, a tab then b, or a space and a tab. Seed 7 adds U+3000, b c, a then a tab, and #c. After the body comes a key, a comment, or nothing.

Run Distinct documents This head disagrees with PyYAML main disagrees with PyYAML
seed 1004 15462 0 5996
seed 7 22902 0 6680

"Agrees" means the same value where PyYAML reads the document, and a refusal where PyYAML refuses it.

The 16 tracked .yaml and .yml files read the same. Each file was read by main's reader and by this head's, raw and with its placeholders filled (shape_materialize.PLACEHOLDER_RE): 32 of 32 readings are equal. Their only headers are | and >-.

The SUPPORTED paragraph

It says what was decided:

  • both chomping indicators are honoured;
  • the indentation indicator is in the REFUSED list, with why it is refused and not read;
  • a new paragraph, "A BLOCK SCALAR'S BODY IS READ AS YAML READS IT, OR REFUSED BY ITS LINE", lists each reading above and names the three functions.

What an adopted project sees

scripts/repo_shape.py is a pinned copy. Each project keeps its old copy until update-shape.py apply brings it to this one, and that re-pins every project once. The old copy reads a digit header with the digit ignored and reads the layouts above as main does, without a word. So nothing changes for a project until it applies.

From then on, the reader in the project's YAML files works as follows:

  • A digit header, row 3, or a tab left of a block's column makes the file yaml-unparsable, naming the file and the line. test_the_projects_own_validator_refuses_a_digit_header pins this: a scaffolded project's own validate-manifest.py exits 2 over a note: |2 in its project.yaml, with REFUSED yaml-unparsable: <root>/project.yaml: line N: a block scalar's indentation indicator is not supported: '|2'.
  • +, a deeper line of spaces, a folded line that opens with a space or a tab, or a block with no text reads with YAML's value, and nothing is printed. A block scalar that holds no text was '\n', a string that is not empty, and is now ''.

None of this reaches a file a tool wrote: no tool writes a block scalar, and the tracked files hold none of these layouts.

Left out of scope

Tests

Every run is under a stub gh first on PATH (exit 97), with GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false and EMAIL=tests@openreposhape.invalid. Runs are one file at a time, each in timeout 2400, as /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<file> -o tmp_path_retention_policy=failed tests/<file>.py. PyYAML 6.0.3 is installed, so every NEEDS_PYYAML comparison ran. All runs were at the head below.

File Result
tests/test_yaml_reader_spaces.py (changed) 513 passed
tests/test_yaml_block_scalar_keys.py (changed) 75 passed
tests/test_yaml_indicator_openers.py 228 passed
tests/test_leg_path_yaml_scalars.py 199 passed, 8 skipped
tests/test_legs_readers_not_a_list.py 74 passed
tests/test_load_yaml_refusals.py 11 passed
tests/test_reader_refusals_named.py 44 passed
tests/test_yaml_flow_brackets.py 55 passed
tests/test_yaml_flow_mappings.py 46 passed
tests/test_yaml_flow_quotes.py 17 passed
tests/test_yaml_quote_boundaries.py 73 passed
tests/test_template_text_placeholders.py 142 passed
tests/test_template_values_single_pass.py 186 passed
tests/test_update_shape_apply_lines_and_headers.py 42 passed
tests/test_update_shape_header_comments.py 54 passed
tests/test_pinned_copy_text.py 87 passed
tests/test_adopt_entry_paths.py 79 passed
tests/test_manifest_validator.py 102 passed
tests/test_bump_leg.py 54 passed, 1 skipped
tests/test_bump_leg_unreadable.py 19 passed
tests/test_pin_rewrite_layout.py 115 passed
tests/test_agents_md_codes.py 47 passed
tests/test_cli_reference.py 23 passed
tests/test_update_add_codes_documented.py 6 passed
tests/test_repo_hygiene.py 100 passed
tests/test_no_auto_maintenance.py 60 passed
tests/test_shape_doctor.py, once, because the doctor imports repo_shape.py 445 passed, 3 skipped

What the new tests pin, in tests/test_yaml_reader_spaces.py. Each PyYAML twin runs under the file's NEEDS_PYYAML guard.

Test Rows Pins
test_an_indentation_digit_is_refused_by_the_headers_line, test_pyyaml_reads_each_digit_header_otherwise_or_refuses_it 10 + 10 SHOULD-1's three rows, #974's row, digit first, nested, in an item, |0, |10, U+0662. PyYAML reads the first seven otherwise (why each is refused) and refuses the last three
test_a_block_scalar_reads_as_yaml_reads_it, test_pyyaml_reads_each_block_scalar_the_same 16 + 16 rows 1 and 2, the tab at the column, and each further reading above
test_a_block_scalar_yaml_refuses_is_refused_by_its_line, test_pyyaml_refuses_each_such_block_scalar_too 11 + 11 rows 3, 4a and 4b, nested, folded, before the next key, the first deeper line named, a tab line first, a blank line one space deeper than the first line (row-3-one-space-deeper), two tabs (row-4b-two-tabs)
test_what_the_tracked_headers_hold_still_reads, test_pyyaml_reads_what_still_reads_the_same 7 + 7 |, >- and > over layouts main already read as YAML does, and a blank line at the first line's column (a-blank-line-at-the-first-lines-column)
test_a_file_holding_one_is_yaml_unparsable_naming_the_line_once[an-indentation-digit] 1 load_yaml names the file and the header's line once, with no remedy
test_the_projects_own_validator_refuses_a_digit_header 1 a scaffolded project's own validate-manifest.py exits 2

Mutants

Each mutant went into scripts/repo_shape.py alone. Each was run over tests/test_yaml_reader_spaces.py and tests/test_yaml_block_scalar_keys.py, with the scaffolding tests deselected (-k "not projects_own and not bootstrap and not bump and not update_shape and not scaffold and not template and not validator and not tracking_branch"). The file was restored after each one, and its sha256 checked the same after the run. All 18 were KILLED at df6e283b. Fix round 1 adds F, F2 and C (RV1021's mutants) below, and remade M5 to M8 on the new helpers, with one more (M19, the blank lines recorded only after the block's column is set). All of those were KILLED too.

Mutant Killed by (first failure)
M1 the header pattern back to main's, so |2- is no header test_an_indentation_digit_is_refused_by_the_headers_line[the-digit-first]
M2 no digit refusal ...[should-1-literal-2] (29 failed)
M3 the digit refused after the body is read ...[the-974-row]
M4 a deeper line of spaces blank again test_a_block_scalar_reads_as_yaml_reads_it[row-2-a-deeper-line-of-spaces]
M5 no refusal of a deeper blank line above the first test_a_block_scalar_yaml_refuses_is_refused_by_its_line[row-3-a-deeper-blank-line-first]
M6 a # under a deeper blank line refused, not a comment test_a_block_scalar_reads_as_yaml_reads_it[a-comment-under-a-deeper-blank-line]
M7 the last deeper blank line named, not the first ...[row-3-the-first-deeper-line-is-named]
M8 a tab left of the column, past the key, blank again ...[row-4b-a-space-and-a-tab]
M9 a tab line no deeper than the key blank again ...[row-4a-a-tab-line] and ...[a-tab-on-the-blocks-first-line]
M10 a tab at the block's column blank again test_a_block_scalar_reads_as_yaml_reads_it[a-tab-at-the-blocks-column]
M11 folding joins every line again ...[row-2-folded], ...[a-deeper-folded-line]
M12 folding asks only the line after ...[a-deeper-folded-line]
M13 a fold over empty lines keeps the break too test_what_the_tracked_headers_hold_still_reads[folded-strip]
M14 + read as clip again test_a_block_scalar_reads_as_yaml_reads_it[row-1-keep]
M15 clip adds a line break to a block with no text ...[clip-with-no-text]
M16 + drops one empty line ...[row-1-keep]
M17 the digit test takes + for a digit ...[row-1-keep] and the |+ plain-twin rows (17 failed)
M18 folding drops the empty lines before the first line test_what_the_tracked_headers_hold_still_reads[folded-after-an-empty-line]
F a blank line one space deeper than the first line is not refused (held > spaces + 1) test_a_block_scalar_yaml_refuses_is_refused_by_its_line[row-3-one-space-deeper]
F2 a blank line at the first line's column taken as deeper (held >= spaces) test_what_the_tracked_headers_hold_still_reads[a-blank-line-at-the-first-lines-column]
C only a lone tab refused as a tab (rest == "\t") test_a_block_scalar_yaml_refuses_is_refused_by_its_line[row-4b-two-tabs]

Fix round 1 (RV1021, RV1021d)

The branch head moves from df6e283b to 9dd0d0af: W1004's fix round 1 (d4440275, reviewed READY by RV1021d), one more commit for RV1021d's NIT (31104324), and a merge of origin/main 051176ce (#1026, which fixed main's two red test rows). The merge was clean. The opening's "One commit" is the first commit; the files are still the same three.

Fixes for RV1021:

  • SHOULD-1: two rows in tests/test_yaml_reader_spaces.py, each with its PyYAML twin: row-3-one-space-deeper (a blank line one space deeper than the first line's, refused at line 2) and a-blank-line-at-the-first-lines-column (reads '\na\n'). They kill RV1021's mutants F and F2.
  • SHOULD-2 (python:S3776): _block_scalar_body's first-line check and its tab and under-indented refusals moved into two helpers, _first_line_is_a_comment and _shallower_line_refusal. Counted by Sonar's rules, which give 20 at df6e283b as Sonar reported, _block_scalar_body is now 12, and the helpers 2 and 1. It is a pure refactor: every blank line is now recorded, but the list is read only while the block's column is unset, so no answer changes.
  • NIT-1: the comments in scripts/repo_shape.py say a block with no text is '' under clip, where it was '\n', and that under - it already was ''.
  • NIT-2: row-4b-two-tabs (k: | over a, a space and two tabs, b) is refused at line 3 as tab used for indentation, and kills RV1021's mutant C.
  • NIT-3: the "Deleting the digit" sentence under "What an adopted project sees" names the - and + headers and the changed layouts.
  • Pin: no tracked file names a digest of scripts/repo_shape.py, so there was nothing to re-pin. test_pinned_copy_text and test_validate_pins_lockstep_lines pass.

Fix for RV1021d (31104324):

  • NIT: the docstring of _shallower_line_refusal said a line that holds a tab and nothing else is refused. The code refuses any line of nothing but tabs and spaces with at least one tab ( \t\t, \t ), so the docstring now says that.

Evidence:

  • Pure refactor: the same value, or the same refusal words, from d4440275 and df6e283b over 5,390,760 documents (RV1021d's two sets) and, in W1004's own check, over 1,120,768, 0 different. RV1021's exhaustive harness (588,756 documents): both readers 0 disagreements with PyYAML. The fuzz table above still holds, and the 16 tracked files still read 32 of 32 the same.
  • Tests: the Tests table above is as of df6e283b except test_yaml_reader_spaces.py, now 513 (507, and 3 rows with their twins). W1004 re-ran the reader, quote, flow, manifest, pin, docs and hygiene files at d4440275 with the same counts. At the merge 9dd0d0af, each alone under the stub gh: test_yaml_reader_spaces 513 passed, test_yaml_block_scalar_keys 75, test_pinned_copy_text 87, test_validate_pins_lockstep_lines 22, test_no_auto_maintenance 60.
  • Mutants: F, F2 and C are in the table above. RV1021d's own mutants were KILLED as well: O1 (names the last deeper blank line, not the first), O2 (a # first line never ends the block) and O3 (the under-indented words with their two numbers swapped).
  • Merges: the "Merges" section below is as of df6e283b.

Merges

git merge-tree --write-tree of this head:

  • With origin/main 2d6f928b: exit 0, tree e0750805, no conflict. Main has two commits past this branch's base, and neither touches these files.
  • With open pull requests: none touches scripts/repo_shape.py, tests/test_yaml_reader_spaces.py or tests/test_yaml_block_scalar_keys.py. I checked the 13 open pull requests' file lists, each under 100 files, so none is cut short.

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

Summary by Sourcery

Make block scalar parsing follow YAML semantics or report a line-specific refusal for unsupported constructs.

Bug Fixes:

  • Align block scalar parsing with YAML behavior for chomping, whitespace, folded content, and tab handling, while rejecting cases that PyYAML also rejects.
  • Reject unsupported block scalar indentation indicators on the header line instead of silently ignoring them.

Enhancements:

  • Document the supported and refused block scalar behavior and expand coverage for nested, folded, literal, and malformed layouts.

Tests:

  • Add comprehensive block scalar behavior tests, including PyYAML comparisons and project-validator refusal checks.

…tation digit (`|2`, `>-2`) by the header's line, honours `+`, keeps the spaces of a line deeper than the block, and refuses a tab left of the block's column and a deeper blank line above its first line of text, by their lines, as YAML reads each (#1004)

The digit was accepted and never read: `k: |2` over `    a` and
`    b` read 'a\nb\n', where YAML reads '  a\n  b\n', and `>-2` was
read as clip. Every header holding a digit, `|2-` and the `|0` and
`|10` YAML refuses among them, is refused by its own line:
`line 1: a block scalar's indentation indicator is not supported: '|2'`.

The body is read by YAML's own steps, each checked against PyYAML 6.0.3:
- `+` keeps the empty lines at the block's end; it was read as clip.
- A line of spaces deeper than the block keeps the spaces past the
  block's column; it was an empty line.
- A blank line above the first line of text, deeper than it, is refused
  by its line, as PyYAML refuses the document; under it, a `#` line is
  a comment that ends the block.
- A line holding a tab left of the block's column and nothing else is
  refused by its line, `tab used for indentation`; a tab at the
  block's column or past it is text.
- What the same reading changes: a folded block keeps the line breaks
  around a line that opens with a space or a tab, where it joined it
  with a space, and a block with no line of text is '' under clip.

The 16 tracked .yaml and .yml files read the same, raw and with their
placeholders filled. A project's copy of the reader changes at its next
`update-shape.py apply`.

Closes #1004

Lane: openreposhape-3 (openRepoShape-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates the YAML reader to refuse block-scalar indentation indicators on their header lines and to parse supported block-scalar bodies with PyYAML-compatible chomping, whitespace, folding, and tab behavior, backed by extensive regression and validator tests.

Sequence diagram for block scalar parsing

sequenceDiagram
    participant Reader as YAMLReader
    participant Header as BlockHeader
    participant Body as BlockScalarBody
    participant Text as BlockScalarText
    Reader->>Header: _BLOCK_HEADER_RE
    Reader->>Header: _refuse_indentation_indicator
    alt indentation digit present
        Header-->>Reader: YamlError on header line
    else supported header
        Reader->>Body: _block_scalar_body
        alt unsupported indentation or tab layout
            Body-->>Reader: YamlError on offending line
        else supported body
            Reader->>Text: _block_scalar_text
            Text->>Text: _fold_block_body
            Text-->>Reader: YAML-compatible scalar value
        end
    end
Loading

Flow diagram for block scalar YAML parsing and refusal

flowchart TD
    Header[Block scalar header] --> Digit{Indentation digit present?}
    Digit -->|Yes| Refuse[Raise YamlError on header line]
    Digit -->|No| Body[Read block scalar body]
    Body --> Layout{YAML-compatible layout?}
    Layout -->|No| LineRefusal[Raise YamlError on offending line]
    Layout -->|Yes| Chomp[Apply chomping indicator]
    Chomp --> Value[Produce scalar value]
Loading

File-Level Changes

Change Details Files
Reject unsupported block-scalar indentation indicators at the header line while preserving recognition of all indicator-bearing header forms.
  • Expanded header matching to cover digit-first indicator forms such as `
2-.</li><li>Added a dedicated refusal with consistent YamlError` wording and header-line location.
  • Updated supported/refused documentation and tokenization flow to perform the refusal before reading the body.
  • Align block-scalar body parsing and output with PyYAML, or reject layouts that PyYAML rejects.
    • Honored + chomping and corrected empty-block results.
    • Preserved deeper whitespace as text and implemented YAML-compatible folded-line behavior.
    • Added line-specific refusals for over-indented leading blank lines and tabs used left of the block column.
    • Added differential-style coverage for literal, folded, nested, sequence, comment, tab, and whitespace cases.
    scripts/repo_shape.py
    tests/test_yaml_reader_spaces.py
    Extend regression coverage for public reader and project-validator behavior without changing existing supported tracked YAML layouts.
    • Moved digit-bearing headers out of the plain-twin success cases and added refusal assertions.
    • Pinned file-level yaml-unparsable diagnostics and scaffolded project validator exit behavior.
    • Verified existing headers and tracked layouts continue to match expected YAML values.
    tests/test_yaml_block_scalar_keys.py
    tests/test_yaml_reader_spaces.py

    Assessment against linked issues

    Issue Objective Addressed Explanation
    #1004 Handle block-scalar indentation indicators correctly by either applying YAML’s indentation semantics or refusing any header containing an indentation digit on the header’s line. ✅
    #1004 Make the reader agree with YAML or refuse the specified block-scalar layouts: preserve + chomping, retain spaces on deeper spaces-only lines, refuse overly indented leading blank lines and tabs left of the block indentation, and treat tabs at or beyond the block indentation as text. ✅
    #1004 Update the supported-YAML documentation and tests to describe and verify the chosen behavior, including compatibility for supported existing YAML. ✅

    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

    @brettheap

    Copy link
    Copy Markdown
    Contributor Author

    (lane openRepoShape-3, 2026-10-20T00:29:17Z) RV1021 review of #1021 at df6e283: READY. No MUST.

    SHOULD-1 (tests/test_yaml_reader_spaces.py:1232, scripts/repo_shape.py:1019-1020): both edges of the deeper-blank-line refusal are untested. Mutant spaces > nxt_indent + 1 survives 1063 reader tests and reads k: | over , a as '\na\n' (the head and PyYAML refuse it). Mutant spaces >= nxt_indent survives the changed files and refuses k: | over , a as "holds 2 spaces, more than the 2" (the head and PyYAML read '\na\n'). Fix: add ("k: |\n \n a", DEEPER_BLANK.format(line=2, spaces=3, first=2)) to REFUSED_BY_ITS_LINE and ("k: |\n \n a", {"k": "\na\n"}) to STILL_READ.
    SHOULD-2 (scripts/repo_shape.py:962): SonarCloud's one new issue on this PR is python:S3776 CRITICAL, cognitive complexity 20 of 15, in _block_scalar_body. The gate passes. Fix: move the first-line-of-text check (the deeper list, its comment break and its refusal) and the last two refusals into helpers.
    NIT-1 (scripts/repo_shape.py:223, :1083-1084): "a block with no line of text is '' under clip or -, where it was '\n'" - under - main already read ''.
    NIT-2: the tab-refusal words are pinned only for a lone tab. Mutant rest == "\t" survives: \t\t left of the column gets #974's under-indentation words. Add a row such as ("k: |\n a\n \t\t\n b", TAB.format(line=3)).
    NIT-3 (PR body): "Deleting the digit gives back the value the old copy read ... The one exception is >-2" is not so. |+2 over a trailing empty line, >2 over a deeper line, |2 over a deeper line of spaces, and |2 with no text each read otherwise once the digit is deleted.

    Checked: each row of #1004 and each decision in the body, under main, the head and PyYAML 6.0.3 - all as the body says, and each refusal names its own line in true words. Differential fuzz, seed 20261020: my harness, 53130 documents, the head disagrees on 143 (all multi-line plain scalars or a key written twice, which main refuses too; 0 misreads, 0 reads of what PyYAML refuses), main on 7801. W1004's harness, same seed: 30214 documents, head 0, main 8860. Exhaustive bodies of up to 3 lines: 588756 documents, head 0, main 270920. Tracked YAML: 16 files, 48 of 48 readings equal (raw and filled), and all 7 files with block headers read successfully.
    Tests, one file at a time under a stub gh: at the head and at head+main, all the reader files, test_pinned_copy_text (87), test_validate_pins_lockstep_lines (22), the four docs guards, test_no_auto_maintenance (60) and test_manifest_validator (102) pass. CI tests: 2 failed, 17619 passed - only main's two red test_family_add_gitlink_and_null_members rows.
    Mutants: W1004's M3, M9 and M16 re-made and killed; RV1021's F, F2 and C survive (above).
    Merges: clean with origin/main e076e3e (tree 013fa779), with #1011 f5749b7, and with every open PR. None touches these three files.
    Landing order: #1021 can land alone with nothing broken. Both orders with #1011 merge to the same tree d4705790, and the pin tests, the reader tests and test_chain_pin_sources pass there. #1011 changes two other pinned copies (validate-repository-naming.py, validate-manifest.py), so land the two back to back once #1011 is green, in either order, with no v1.2.0 tag between them, so a project that runs apply re-pins once.
    Lane: openreposhape-3 (openRepoShape-3)

    …r-line checks move into two helpers (cognitive complexity 20 to 12), the no-text comment says `-` already read '', and three rows pin a blank line one space deeper, one at the first line's column and two tabs (#1004)
    
    Fix round 1 for #1021, from RV1021's review (issuecomment-6280423796):
    
    - SHOULD-1: `row-3-one-space-deeper` (refused at line 2, 3 spaces over
      a first line of 2) and `a-blank-line-at-the-first-lines-column` (read
      '\na\n'), each with its PyYAML twin. RV1021's mutants F
      (`> spaces + 1`) and F2 (`>= spaces`) are killed by them.
    - SHOULD-2: SonarCloud python:S3776, `_block_scalar_body` at 20 of 15.
      The first line of text's check is `_first_line_is_a_comment`, and the
      tab and under-indented refusals are `_shallower_line_refusal`; every
      blank line is recorded, because only the first line's check reads the
      list and it runs only before the block's column is set. By Sonar's
      rules the function now scores 12, the helpers 2 and 1. A pure
      refactor: over 1120768 documents (RV1021's exhaustive bodies with two
      more tab pieces, and both fuzz seeds) the reader answers word for
      word as df6e283 did, and the fuzz, RV1021's exhaustive harness and
      the tracked-YAML comparison give the same counts, 0 disagreements
      with PyYAML.
    - NIT-1: a block with no line of text is '' under clip, where it was
      '\n'; under `-` main already read '' (the SUPPORTED paragraph and
      `_block_scalar_text`'s docstring).
    - NIT-2: `row-4b-two-tabs`, ` \t\t` left of the block's column, pins
      the tab words beyond a lone tab, and kills RV1021's mutant C.
    
    Part of #1004
    
    Lane: openreposhape-3 (openRepoShape-3)
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Brett Heap and others added 2 commits October 20, 2026 01:45
    …ine of nothing but tabs and spaces, at least one a tab, is refused, as the code does, not a line that holds a tab and nothing else (#1004)
    
    RV1021d's NIT: the code at `rest.strip(_YAML_WHITE) == ""` refuses ` \t\t` and `\t ` as well as a lone tab, so the docstring named only the single-tab rule that mutant C uses.
    
    Lane: openreposhape-3 (openRepoShape-3)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    …indicators
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    @sonarqubecloud

    Copy link
    Copy Markdown

    @brettheap

    Copy link
    Copy Markdown
    Contributor Author

    (lane openRepoShape-3, 2026-10-20T01:54:09Z) #1021 is pushed at 9dd0d0af (9dd0d0af35d618cc0b202f031b105ebd38d38ed7): fix round 1 (d4440275, below), RV1021d's docstring NIT (31104324), and a clean merge of main 051176ce. The merge tree is cb4045df, the same as git merge-tree predicts. RV1021d: READY.

    W1004, fix round 1:
    W1004 fix round 1 for RV1021's review (issuecomment-6280423796). Committed locally as d444027 on top of df6e283; it will be pushed with a merge of main once #1026 lands, so this PR's head on GitHub is still df6e283.

    SHOULD-1: tests/test_yaml_reader_spaces.py gains row-3-one-space-deeper (refused at line 2, "holds 3 spaces, more than the 2 of that line") and a-blank-line-at-the-first-lines-column (reads '\na\n'), each with its PyYAML twin. RV1021's mutant F (held > spaces + 1) is killed by the first and F2 (held >= spaces) by the second.
    SHOULD-2 (python:S3776): _block_scalar_body's first-line-of-text check is now _first_line_is_a_comment, and its tab and under-indented refusals are _shallower_line_refusal; every blank line is recorded, because only the first-line check reads the list and it runs only before the block's column is set. Counted by Sonar's rules (+1 plus nesting for if/while, +1 for elif/else, +1 per boolean run; the count gives 20 at df6e283, as Sonar did): 12 now, the helpers 2 and 1. A pure refactor: the working tree answers 1120768 documents (RV1021's exhaustive bodies with two more tab pieces, and fuzz seeds 1004 and 7) word for word as df6e283 does; the fuzz (15462 and 22902 documents), RV1021-exhaust.py (588756) and the tracked-YAML comparison (32 of 32) each give 0 disagreements with PyYAML, as before.
    NIT-1: scripts/repo_shape.py:223-224 and :1114-1116 now say a block with no text is '' under clip, where it was '\n', and that under - it already was ''.
    NIT-2: row-4b-two-tabs (" \t\t" left of the block's column, "line 3: tab used for indentation") kills mutant C (rest == "\t").
    NIT-3: the body's "Deleting the digit" sentence is corrected: it now names the -/+ headers and the changed layouts, with RV1021's rows.
    Pin: no tracked file names a digest of scripts/repo_shape.py (main's, df6e283's or the new one), so there is nothing to re-pin; test_pinned_copy_text (87) and test_validate_pins_lockstep_lines (22) pass.
    Tests, one file at a time under a stub gh: test_yaml_reader_spaces 513, test_yaml_block_scalar_keys 75, test_yaml_indicator_openers 228, the four flow and quote files 55/46/17/73, test_leg_path_yaml_scalars 199 passed 8 skipped, test_load_yaml_refusals 11, test_reader_refusals_named 44, test_bom_reads 25, test_legs_readers_not_a_list 74, test_manifest_validator 102, the three -k rows 1/2/1, the docs guards 47/23/6/100, test_no_auto_maintenance 60. All pass.
    Mutants: F, F2 and C killed; W1004's M5-M8 remade on the helpers and one more on the blank-line list, all killed.

    RV1021d, delta review:
    RV1021d: delta review of W1004's fix round 1, d444027 on top of df6e283 (local, not yet pushed). Verdict: READY. No MUST, no SHOULD, one NIT.

    Fixes against RV1021: SHOULD-1 rows row-3-one-space-deeper (tests/test_yaml_reader_spaces.py:1249) and a-blank-line-at-the-first-lines-column (:1290) are RV1021's rows exactly. SHOULD-2: _first_line_is_a_comment (scripts/repo_shape.py:1032) and _shallower_line_refusal (:1057) give the old answers branch for branch; every blank line is now recorded (:1014), but the list is read only while the block's column is unset, so no answer changes. NIT-1 (:223-231, :1114-1116) checked against main e076e3e: k: | over an empty line was '\n' and is now '', k: |- was already ''. NIT-2: row-4b-two-tabs (:1257). NIT-3: the live body's "Deleting the digit" sentence is correct; main read |-2, >-2 and |+2 as clip, refused |2- at line 2, and read the >2 example as 'a b c\n'.

    Cognitive complexity (S3776 rules, counted by hand): _block_scalar_body 12 (while 1; if-and at nesting 1, 3; three elifs, 3; nested if at nesting 2, 3; elif and else, 2), _first_line_is_a_comment 2, _shallower_line_refusal 1. The same rules give df6e283's 20, as Sonar did.

    Pure refactor: 5,390,760 documents get the same value repr or the same exception words from d444027 and df6e283, 0 different: RV1021's exhaustive alphabet plus four tab pieces (2,441,352), and 1-3 blank lines of 0-7 spaces or a tab above a first line of text at columns 1-6 (2,949,408). These reach every outcome of the moved code: text 1,002,288, comment ends 214,620, refused 1,141,812, tab refusal 167,472, under-indented 107,304. RV1021-exhaust.py (588,756 documents): both readers 0 disagreements with PyYAML.

    Mutants (two reader files, each alone, sha restored): F held > spaces + 1 killed by [row-3-one-space-deeper]; F2 held >= spaces killed by [a-blank-line-at-the-first-lines-column]; C rest == "\t" killed by [row-4b-two-tabs]; my own: deeper[-1] named, killed by [row-3-the-first-deeper-line-is-named]; a # first line never ends the block, killed by [a-comment-under-a-deeper-blank-line]; the under-indented words with their numbers swapped, killed by 10 rows.

    Tests at d444027, one file at a time under a stub gh: test_yaml_reader_spaces 513 passed, test_yaml_block_scalar_keys 75 passed, test_pinned_copy_text 87 passed, test_validate_pins_lockstep_lines 22 passed, test_no_auto_maintenance 60 passed.

    Merges (git merge-tree --write-tree): with origin/main e076e3e exit 0 (b6f51f40); with #1011 f5749b7 exit 0 (5df16251); with main and #1011 together exit 0 (a72ef038).

    NIT (scripts/repo_shape.py:1063): _shallower_line_refusal's docstring says "A line that holds a tab and nothing else", but the code refuses any line of only tabs and spaces (" \t\t", "\t "). Suggest "A line that holds nothing but tabs and spaces (at least one tab)".

    Skipped to save the window: the other reader files (W1004 ran them this round), a PyYAML comparison over the second set, a Sonar re-analysis (not pushed), test_shape_doctor and an apply end to end.

    Lane: openreposhape-3 (openRepoShape-3)

    @sourcery-ai

    sourcery-ai Bot commented Oct 20, 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 1 day and 15 hours by commenting @sourcery-ai review. Upgrade to get a review now.

    @brettheap
    brettheap merged commit d3e1e49 into main Oct 20, 2026
    5 checks passed
    @brettheap
    brettheap deleted the l3-1004-block-scalar-indicators branch October 20, 2026 04:02
    brettheap pushed a commit that referenced this pull request Oct 20, 2026
    …lict, so #1025 is tested against main as it stands, with #983's change to validate-family.py and #1026's fix of main's two red rows (#936)
    
    Main took twelve squashes since e076e3e: #1026, #958, #986, #1002,
    #1016, #1011, #1021, #1022, #973, #975, #1015 and #983. Only #983
    touches validate-family.py, in `_git_runs` and the member checks, apart
    from this branch's hunks; it touches none of this branch's test files.
    
    Lane: openreposhape-4 (openRepoShape-4)
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    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