Skip to content

validate-repository-naming.py: every name the gate tables or refuses prints through spelled(), so a name with a line feed or an ESC byte is one row and one naming-unclassified name, with its column worked out from the spelled form (#1267) - #1271

Draft
brettheap wants to merge 2 commits into
fix/1202-naming-fullmatchfrom
fix/1267-naming-spelled

Conversation

@brettheap

@brettheap brettheap commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Goal

#1267: validate-repository-naming.py prints every name it tables or refuses through repo_shape.spelled(), so a name that holds a line feed or another control byte stays on one line and in one table row, and the finding for Atlas plus a line feed cannot read as the finding for a valid Atlas.

This PR is stacked on #1266 (#1202) and lands after #1266's squash, by a restack. Its base is fix/1202-naming-fullmatch (#1266's head 09697e9c5), not main. It is a DRAFT. The change is two commits on top of 09697e9c5, 4b1653d8b and the review round 4fa9f78a6 (see "Review round"): 3 files, +387 and -19 (git diff --shortstat 09697e9c5..HEAD): scripts/validate-repository-naming.py (+28 -15), the new tests/test_naming_gate_spelled.py (+350, 36 rows), and tests/test_naming_policy_line_break.py (+9 -4: two of #1266's rows now assert the spelled form, see "What changed").

It is the follow-up lane 5's independent review of #1266 filed as #1267 (NIT 2, #1266 issuecomment-6724498649). #1266 makes Atlas plus a line feed a name of no family; this one is about how the gate then prints that name.

Review round (rv1271: six NITs)

rv1271 (opus, independent) found this PR READY at 4b1653d8b with six NITs, none blocking (#1271 issuecomment-6726218829). The round is one commit on it, 4fa9f78a6: 2 files, +39 and -20 (scripts/validate-repository-naming.py +13 -10, tests/test_naming_gate_spelled.py +26 -10). NITs 1 to 5 are done. NIT 6 is left for the restack.

NIT What rv1271 found What was done
1 The module docstring said two things that cannot both be true: a chain link and a caller's words are "printed as given", and "every repository name this prints" is spelled Docstring only (the script's syntax tree outside the docstring is equal to 4b1653d8b's, proved with ast). It now says one thing: every name the gate classifies or refuses is spelled, and a sentence that quotes one is spelled whole, so a chain link or a caller's --link-remedy words inside it are spelled too, while the CHAIN line, link-source-unreadable and the NOTEs print a chain link as given. The old "printed as given" lines no longer list a chain link or the words of a remedy
2 Two rival escapers survived every row: X1 (escape only a line feed and an ESC) and X3 (repr(value)[1:-1]) Four new rows, below. X1 now fails 2 rows and X3 fails 2, each mutant applied alone and restored with a sha256 check
3 A surrogate name's row changed, against "byte for byte as before" Body only. "What changed" now says the name's bytes are unchanged and its label moved from character 40 to 35
4 The skips were explained wrongly: the body said the reasons "were not printed" Body only. "Runs" names each reason and says why the first gate's logs hid them (run10.sh's -rs, then the gate's -rf: pytest keeps only the last -r)
5 The Merges section was stale Body only. "Merges" is re-measured at the round's head against main fetched fresh
6 closingIssuesReferences is [] while the base is #1266's branch Left for the restack. After it, check that the PR reads #1267

NIT 1, measured. At the round's head, with a chain link openX<ESC>dox and --link-remedy 'openXdox=run<TAB>make <ESC>[31mfetch' (logs/docstring-claims-head.log, logs/note-claims-head.log): the stderr WARNING <name>: <sentence> and --explain's WARNING spell the link (openX\x1bdox) and the remedy words (run\tmake \x1b[31mfetch); --explain's CHAIN line, the link-source-unreadable WARNING (could not answer for openX<ESC>dox) and the NOTEs that name a link (no name classified here reads openX<ESC>dox's own declaration, links: openX<ESC>dox, openDox) print the link with its ESC byte. The values of --link-source and --link-remedy go through _shown, as the docstring's paragraph above the new one says. The "Limits" bullet on chain links still holds as written; the remedy words are the half it did not name.

NIT 2, the rows. SPELLED_ITSELF in tests/test_naming_gate_spelled.py adds two names to the finding row and the table row on the shipped contract (the file is 36 rows now). The expected spellings are written out by hand, as the file's others are:

Name Spelled Pins
Atlas plus a CR (ends-in-cr) Atlas\r the CR: a gate that escapes only a line feed and an ESC prints it raw
At\las plus a line feed (backslash) At\las\n the backslash: spelled prints it as it is, so repr doubles it (and an escaper of every non-ASCII character does too); the line feed beside it keeps the row failing on the base

A ZWJ was not used for the table row: lane 3's #1217 pads by terminal columns and a ZWJ (category Cf) takes none, so its row would change on the restack (ZERO_COLUMN_CATEGORIES at 352dcbc96). A backslash is ASCII and one column.

  • At the head: the 4 rows pass (tests/test_naming_gate_spelled.py 36 passed, logs/new-file-head.log).
  • On the base (09697e9c5's script): the 4 fail, the base prints the CR raw and splits the backslash name's row and finding at its line feed (logs/run-base.log, logs/new-names.log). On main (dff8fd4ba): the same 4 fail (logs/run-main-gate-spelled.log).
  • Under X1 (spelled replaced by a local escaper of a line feed and an ESC): 2 failed, the finding row and the table row of ends-in-cr; the other 40 of the 42 core tests pass. Under X3 (repr(str(value))[1:-1]): 2 failed, the finding row and the table row of backslash; the other 40 pass. Each applied alone to the script in n1267-mut and restored from git, restored x1-narrow sha256 ok, restored x3-repr-unquoted sha256 ok (logs/mutants-core.out, logs/mut-core-x1-narrow.log, logs/mut-core-x3-repr-unquoted.log). No other row fails under either mutant.

What held at #1266's head

Measured at 09697e9c5 (Python 3.12.3, git 2.43.0): the gate printed a name raw.

  • validate-repository-naming.py given Atlas plus a line feed: exit 1, stdout ' Atlas' and then ' NO FAMILY' on the next line (the row split in two), stderr FINDING naming-unclassified: Atlas and then a blank line, which reads exactly as the finding for a valid Atlas.
  • At, a line feed, las: the same split.
  • At, ESC, [31mlas: the ESC byte reached the terminal in the row and in the finding.
  • A name with a CR (Atlas-spec plus a carriage return) printed the same way, the CR raw in the row and in the finding.

On main (without #1266) Atlas plus a line feed is not refused at all: it exits 0, classified as Atlas, with its row split in two (#1266's body has that measurement).

The print sites

Every print of the script, and every string it builds from a name, at the base (09697e9c5), with what became of it. Ten changed:

# Site (base line) What it prints Row
1 _print_classification (1297) the plain listing's row, <name padded to 32> <label> table rows, classified-name row
2 _no_family_lines (791) --explain's header for a name of no family explain NO FAMILY
3 _describe (955) --explain's header for a classified name explain classified
4 _referent_claim_note (831) [a CLAIM: needs a declared pin on openRouter or openxRouter ...], the referents derived from the name claim note
5 _referent_lines (935) --explain's WARNING <sentence>; the sentence quotes the name explain WARNING
6 _referent_lines (937) --explain's OVERLAP <reason>; a broken chain's reason quotes the name explain OVERLAP
7 _classify_targets (1321) the stderr line WARNING <name>: <sentence>; the sentence quotes the name again stderr WARNING
8 _leg_target (1112) FINDING <name>: declared role 'code' but the name is the 'assembly' form ... role finding
9 _leg_target (1119) FINDING <name>: <broken chain's reason>; the reason quotes the name broken-chain finding
10 _emit_reports (1358) FINDING naming-unclassified: <name>, <name> the finding rows

Sites 1 to 4, 8 and 10 spell the name (1 pads the SPELLED name: f" {spelled(name):<32} ...", so the width is worked out from the printed form). Sites 5, 6, 7 and 9 spell the WHOLE string: the sentence resolve_referent builds says the name again (it ends at openQ, but <name> would need openDox or openxDox), so spelling only the prefix leaves the name raw in the same line. (A mutant that spells only the name, M7b and M9b, is killed.)

Not changed, and why (each prints no repository name):

What changed

  • scripts/validate-repository-naming.py: spelled is imported from repo_shape and applied at the ten sites; the module docstring says so beside the _shown paragraph, and that a name with no control prints as it did. spelled is in every kept library: scripts/repo_shape.py at v1.0.0, v1.0.1, v1.1.0, v1.1.1 and v1.2.0 each define it, with the same text (the sha256 of the AST source of spelled and of its two constants, ESCAPED_CATEGORIES and ESCAPED_BIDI_CONTROLS, is equal in all five and at the head), so the import is not skewed (tests/test_pinned_copy_text.py's NEWER_THAN_A_KEPT_LIBRARY names only shown_path and host_shell_dialect, and passes unchanged).
  • No refusal code, flag, command or exit status changes. naming-unclassified stays; only the name in its words is spelled. A name without a line feed, CR, ESC or other control prints as before (the plain-name row, and the 3,563 gate tests below), with one change in a ROW and none in the name: a name with a lone surrogate (Atl\udcffas, in test_legs_not_a_list.py) prints its own bytes as before (spelled writes \udcff, as stderr's backslashreplace did), but its label moved from character 40 to character 35, because the column is now worked out from the spelled name and so lines up with every other row's (measured on 09697e9c5, 4b1653d8b and the round's head: logs/surrogate-label.log; rv1271's NIT 3).
  • tests/test_naming_policy_line_break.py (scripts/repo_shape.py: NamingPolicy reads a name with fullmatch at four sites, so a name that ends in a line feed matches no family and validate-repository-naming.py refuses it, and repository-naming.yaml says a pattern is read whole (#1202) #1266's file), two rows updated: test_the_naming_gate_refuses_a_leg_repository_that_ends_in_a_line_feed (3 rows) and test_the_naming_gate_refuses_an_argv_name_that_ends_in_a_line_feed (3 rows) asserted FINDING naming-unclassified: Atlas, the raw form (the line feed ended the first line). They assert FINDING naming-unclassified: Atlas\n now (a backslash and an n), which is what the issue asks. The docstrings say so. No other row of the file changed.
  • What pins the printed forms (grepped first). tests/, README.md, docs/ (the handbook included) and AGENTS.md for naming-unclassified, NO FAMILY, declared role, also_matches and the table's columns: README, docs and AGENTS name the command only, never a printed name with a control; the tests that pin the table or the finding use printable names (test_chain_link_fold.py, test_naming_policy.py, test_setup_project_name_edges.py with its trailing space), which spelled leaves alone, or Atl\udcffas (test_legs_not_a_list.py), which spelled writes as \udcff exactly as stderr's backslashreplace did. The files that run the gate or read its output all pass (Runs).

The rows

tests/test_naming_gate_spelled.py (new, 36 rows). The expected spellings are written out by hand (Atlas\n, At\nlas, At\x1b[31mlas, Atlas\r, At\las\n), not computed by the library under test.

  • On the shipped contract (16 rows): for each of the issue's three names, the finding is exactly three stderr lines (the finding on one line with the spelled name, then the two lines a plain unclassified name gets), the stdout is exactly one row <spelled padded to 32> NO FAMILY, and --explain's header is <spelled>: NO FAMILY with every later line indented; one run of all three plus a plain Atlas gives four rows with their labels in one column and one finding naming all three; a name on stdin (an ESC; stdin strips a line feed) is spelled too; and a control row, a run of five plain names, prints exactly as it did. Two more names pin spelled itself (rv1271's NIT 2; the finding row and the table row of each, 4 rows): Atlas plus a CR, which spelled writes \r (a gate that escaped only a line feed and an ESC prints it raw), and At\las plus a line feed, written At\las\n (a backslash prints as it is; repr would double it).
  • On a fork's contract (20 rows): the shipped contract classifies none of the three names (repo_shape.py: NamingPolicy reads a name with .match against patterns that end in $, so a name that ends in a line feed is classified as the name without it, and validate-repository-naming.py exits 0 over such a leg repository (#1166 follow-up) #1202), so sites 3 to 9 print a name only when a fork's patterns take an ESC and a line feed. The rows read the shipped contract with its 12 character classes [A-Za-z0-9] widened (loosened() asserts the count), and ask: the listing row and the header of a classified name (3 + 3 rows), the claim note's derived referents (3), the stderr WARNING (2), --explain's WARNING and OVERLAP (2 + 2), the role finding (3) and the broken-chain finding (2). A descendant name ends in its product, which the direct pin reads, so the control is in its stem there (inner line feed and ESC); the other rows ask all three names.

Failing on the base

The base is 09697e9c5's script (sha256 fc5c9f81c953), swapped into n1267-mut (detached at the round's head 4fa9f78a6, so the new file and the updated rows are the branch's) and restored from git with a sha256 check (0b6b187ee256, the branch's script; logs/run-base.log):

Mutants

Each applied alone to the branch's scripts/validate-repository-naming.py in n1267-mut (detached at the round's head 4fa9f78a6), run against tests/test_naming_gate_spelled.py (36 rows) and the 6 updated rows of tests/test_naming_policy_line_break.py (42 tests; the first commit's run over the whole two files, 109 rows, gave the same counts), and restored from git with a sha256 check (restored ... sha256 ok after each, and the script's bytes equal the branch's at the end; logs/mutants-core.out):

Mutant Rows failing Killed by
M1: site 1 prints the raw name ({name:<32}) 15 the 5 table rows, the 4-row run, stdin, the 3 classified-listing rows, the stderr WARNING (2), the role finding (3)
M1w: site 1 spells the name but pads by the RAW name's length (32 - len(name)) 15 the same 15: the escape is longer than the byte it writes, so every label stands 1 or 3 columns off
M2: site 2 raw 3 the 3 --explain NO FAMILY rows
M3: site 3 raw 7 the 3 classified-header rows, and 4 more that read the header line (2 claim rows, 2 --explain WARNING rows)
M4: site 4 raw (the claim note's referents) 3 the 3 claim rows
M5: site 5 raw (--explain WARNING) 2 the 2 --explain WARNING rows
M6: site 6 raw (--explain OVERLAP) 4 the 2 OVERLAP rows and the claim rows whose OVERLAP line splits (inner line feed, trailing line feed)
M7: site 7 raw (stderr WARNING) 2 the 2 stderr WARNING rows
M7b: site 7 spells the name only, not the sentence 2 the same 2 (the sentence says the name again)
M8: site 8 raw (role finding) 3 the 3 role-finding rows
M9: site 9 raw (broken-chain finding) 2 the 2 broken-chain rows
M9b: site 9 spells the name only 2 the same 2
M10: site 10 raw (", ".join(unclassified)) 13 the 5 finding rows, the 4-row run, stdin, and #1266's 6 updated rows
M11: spelled replaced by repr 42 all 36 rows of the new file, plain control included, and the 6 updated rows (the quotes and the doubled backslash are not the spelling)
X1 (rv1271's): spelled is a local escaper of a line feed and an ESC only 2 the finding row and the table row of ends-in-cr (round)
X3 (rv1271's): spelled is repr(str(value))[1:-1] 2 the finding row and the table row of backslash (round)

rv1271's other mutants are killed too, with the round's rows: X2 (escape every non-ASCII character; the backslash rows kill it as well) 10, X4 (spell the whole naming-unclassified block, its line breaks too) 13, X5 (site 9 spells the reason and not the name) 2, X6 (site 7 spells the sentence and not the name) 2, X7 (the claim note spells its first referent only) 3, X8 (pad the raw name, then spell it) 15.

X1 and X3 survived every row of the first commit (rv1271's NIT 2); the round's 4 rows kill them, and no other row does. No survivor.

Runs

Python 3.12.3 (CI runs 3.14; the rows assert lists and lines, nothing that depends on a repr or an ast.dump), through lane 5's run10.sh (gh stub first on PATH, bogus token, empty GH_CONFIG_DIR, blank git identity, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null), git-filter-repo 2.47.0 (CI's pin; git filter-repo --version prints a40bce548d2c), at head 4b1653d8b, dirty=0. The gate list is the 37 test files that name validate-repository-naming or pin its printed forms (grep -l), this PR's three, tests/test_naming_policy.py, and the hygiene five (test_path_guard_hygiene, test_repo_hygiene, test_agents_md_codes, test_cli_reference, test_pinned_copy_text): 3,567 tests in 28 runs, none of more than 100 tests beside another (one exception, below).

Run git Result
the gate list (28 runs) 2.43.0 3,563 passed, 4 skipped, 0 failed
the gate list (28 runs) 2.47.1 (tools/git247/bin/git, built from kernel.org's tarball) 3,560 passed, 7 skipped, 0 failed (3 more skips than the 2.43.0 leg: test_adopt_check_throwaway_repos.py:1041, below)
tests/test_naming_gate_spelled.py alone, before the commit 2.43.0 32 passed
the naming files, test_naming_policy, the hygiene five and test_chain_link_fold, on the PREDICTED merge of current main (51a9c5f10) and the branch (merge-tree and commit-tree: an object, no ref) 2.43.0 769 passed in 92.43s

The skips, by reason. rv1271 measured them. The first gate's logs hid them because run10.sh passes -rs and the gate runs then passed -rf, and pytest keeps only the LAST -r option, so no reason was printed. On 2.43.0 the 4 skipped are tests/test_shape_doctor.py:10516 x3 ("the PowerShell line is printed, and powershell.exe exists, only on a Windows host") and tests/test_adopt_check_throwaway_repos.py:482 x1 ("needs a disk that ignores case: git init wrote no core.ignorecase=true in the work directory, so that disk keeps case; the rows above write the setting into the throwaway repository by hand instead"). On 2.47.1 the 7 are those 4 and tests/test_adopt_check_throwaway_repos.py:1041 x3 ("this git's git clone takes init.defaultBranch = a..b, as 2.55's does: the split clones there"); 2.43.0's clone does not take it, so its rows run. I ran those two files again at the round's head with -rs alone, on both gits, and the same lines print (logs/skips-243.log, logs/skips-2471.log). A slip in the load rule (first gate): the 106-test run of the #1236 check below ran while the gate's slot A held a run of more than 100 tests; it passed.

The review round's runs. Head 4fa9f78a6, dirty=0, Python 3.12.3, git-filter-repo 2.47.0 (a40bce548d2c), through run10.sh with its -rs and no other -r, so a skip would name its reason. The files are the brief's ten: the new file, test_naming_policy_line_break, test_naming_policy, test_chain_link_fold, test_legs_not_a_list and the hygiene five (test_path_guard_hygiene, test_repo_hygiene, test_agents_md_codes, test_cli_reference, test_pinned_copy_text): 885 tests in 8 runs per git, none of more than 100 tests beside another.

Run git Result
the ten files (8 runs) 2.43.0 (/usr/bin/git) 885 passed, 0 skipped, 0 failed
the ten files (8 runs) 2.47.1 (tools/git247/bin/git) 885 passed, 0 skipped, 0 failed
tests/test_naming_gate_spelled.py alone, before the round's commit 2.43.0 36 passed

Logs: gate/gate-243-*.log, gate/gate-2471-*.log, gate/slotA.out, gate/slotB.out. The round changes the script's docstring and the new test file only, so the first commit's 37-file gate above stands for the rest. A slip in the load rule: the resolved-#1236 check (Merges) ran its 154-test chunk while the skip re-run above held a 152-test file; both passed. Not run: the full suite, a real Windows host and macOS.

Merges

git merge-tree --write-tree <other> 4fa9f78a6 (git 2.43.0) at 2026-11-08T20:07Z, against current main (fetched fresh at 20:07Z), #1266's head and every open PR that touches one of this PR's three files (47 open when listed at 19:56Z; only #1266 and #1236 do; open-prs.json, logs/merge-trees-2.txt; the first read at 19:56Z, logs/merge-trees.txt, gave the same trees for #1266 and #1236 and main dff8fd4ba clean at 27441e98):

Against Head Tree
main 40cb8086f b6cfa9c6 (clean)
#1266 (tests/test_naming_policy_line_break.py; this PR's parent) 09697e9c5 264fe0fb, the branch's own tree: the head descends from it
#1236 (scripts/validate-repository-naming.py, lane 3's #1217 display-width pad) 352dcbc96 CONFLICT, one hunk, tree 7c6acc90; stage blobs 4f3d7977 (base), ff33b440 (this branch), 4bfafa63 (#1236)

Main moved since the first commit (51a9c5f10 to 40cb8086f); none of its moves touches scripts/validate-repository-naming.py, scripts/repo_shape.py or the two test files (git log 4b1653d8b..origin/main over them is empty). #1236 (352dcbc96) and #1266 (09697e9c5) have not moved, read again at 20:07Z.

#1236 still conflicts at _print_classification's print line, and only there: one conflict marker pair in the merged file (lines 1386 to 1392), and the round's docstring reword merges clean with #1236's changes. Whichever lands second takes the refresh. #1236 pads by the columns a terminal gives the name as stdout writes it (_padded(_printed(name), 32)); this PR spells the name ({spelled(name):<32}). The resolution composes them, spell first and then pad what is printed: print(f" {_padded(_printed(spelled(name)), 32)} {label}{also}"). I rebuilt it as a commit object with no ref (2ad9b0832: the branch and #1236's head, the print line resolved as above) in n1267-mut and ran, on it, this PR's new file with test_chain_link_fold.py (36 + 19 = 55 tests), #1236's test_pinned_copy_columns.py (55) and test_siblings_family_name.py (154), and test_naming_policy_line_break.py (77): 341 passed in 4 runs (logs/run-resolved-1236-a.log to -d.log). The other hunks of #1236 merge clean. The mut tree was put back at 4fa9f78a6, clean, its script's sha256 equal to the branch's.

Limits

  • Sites 3 to 9 print a control only under a fork's contract. The shipped patterns take [A-Za-z0-9] and hyphens, so on the shipped data a name with a control is never classified and only sites 1, 2 and 10 print it. The other sites are spelled for the same reason the first three are (one rule: a repository name the gate prints is spelled), and their rows read a fork's contract. No shipped project sees them change.
  • Chain links and --pins values are printed as given (the module docstring's standing rule; a manifest's are checked printable, a command line's are the caller's words). So --explain's CHAIN line prints a link raw, while the four sentences that quote it (sites 5, 6, 7, 9) are spelled whole and so escape a control in a link too. Not changed here.
  • A manifest leg's role: prints raw in --explain's [ADMITS ...; <role> is declared here and is not admitted] note (--role is a closed choice; a manifest's value is not). It is a role, not a repository name, so it is outside validate-repository-naming.py: a name that holds a line feed or another control byte is printed raw, in the table and in naming-unclassified, so the finding for Atlas plus a line feed reads like one for a valid Atlas #1267. Found, not fixed.
  • A name read from stdin is stripped, so a trailing line feed reaches the gate only from argv or a manifest; the stdin row asks for an ESC.
  • A project's own copy of validate-repository-naming.py reaches this through update-shape.py; it needs spelled in its scripts/repo_shape.py, which every kept library has.
  • shape-doctor.py and the setup, scaffold and adopt tools run the gate and quote its lines; they print what it prints, so a name with a control is spelled in their rows too. Their tests are in the gate list above.

Closes #1267

Lane: openreposhape-5 (openRepoShape-5)

🤖 Generated with Claude Code

Summary by Sourcery

Render every repository name reported by the naming gate through its escaped representation so control characters cannot split output rows or make findings ambiguous.

Bug Fixes:

  • Ensure repository names containing line feeds, escape characters, carriage returns, or other control characters remain single-line and unambiguous in naming-gate output.

Enhancements:

  • Apply consistent escaped-name rendering across listings, explanations, warnings, role findings, broken-chain findings, and unclassified-name reports while preserving column alignment.
  • Add comprehensive coverage for escaped names, classified and unclassified output, stdin, project manifests, explanations, and finding messages.

Documentation:

  • Document the naming gate’s escaped rendering behavior and the distinction between repository names and other displayed values.

Tests:

  • Add regression tests covering control characters, whole-sentence escaping, output alignment, and unchanged rendering of ordinary names.
  • Update line-break naming-policy expectations to assert escaped repository names.

…prints through `spelled()`, so a name with a line feed or an ESC byte is one row and one `naming-unclassified` name, with its column worked out from the spelled form (#1267)

With #1266 (#1202) a name that ends in a line feed is refused
`naming-unclassified`, but the gate printed it raw: the table row split in
two, and `FINDING naming-unclassified: Atlas` was followed by a blank line,
which reads as the finding for a valid `Atlas`. A name with an inner line
feed split the same way and an ESC byte reached the terminal.

Ten print sites now spell the name (`repo_shape.spelled`, in every tag since
v1.0.0, so the import is not skewed): the plain listing, whose label column is
worked out from the spelled name; the `--explain` NO FAMILY and classified
headers, CLAIM note, WARNING and OVERLAP lines; the stderr
`WARNING <name>: <sentence>` line; the role finding and the broken-chain
finding of `--project`, whose sentence quotes the name, so the whole line is
spelled; and the `naming-unclassified` finding. No code, flag or exit status
changes, and a name without a control prints byte for byte as it did.

tests/test_naming_gate_spelled.py holds the rows (31 of 32 fail on #1266's
head). tests/test_naming_policy_line_break.py's two rows that asserted the raw
finding now assert the spelled one.

Closes #1267

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 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

UI

Spell repository names in naming-gate output, including complete sentences that quote them, and pad listing rows using the spelled form so control characters cannot split rows or findings.

  • scripts/validate-repository-naming.py L101-115 (+10 small edits)

Tests

Add coverage for spelled names across listing, explain, warning, and finding output, and update line-break tests to expect the escaped finding form.

  • tests/test_naming_gate_spelled.py L1-350
  • tests/test_naming_policy_line_break.py L209-211, L217-218, L242-247

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-5, 2026-11-08T19:44Z) Independent review of #1271 (#1267) by rv1271, lane 5's opus reviewer, which wrote none of it: READY at 4b1653d8, with six NITs. A small round for NITs 1-5 follows, since #1271 lands after #1266 anyway. The report follows as the reviewer wrote it.

Independent review of #1271 (#1267) for lane openRepoShape-5, by rv1271 (opus), at 4b1653d: READY

There are no MUST or SHOULD findings. The ten print sites are the right set, and every count the body gives reproduces: rows, mutants, gates and skips. There are 6 NITs, none blocking. CI's tests job was still pending at 19:43Z (SonarCloud, package and tests-git-bash pass), so the landing needs it green.

Scope: one commit, 4b1653d8be69223969ca5c5eec4dfba20dc0d5c5, with 3 files at 22/12, 334/0 and 9/4 by numstat. Its parent is #1266's head 09697e9c5. By ls-remote the head equals refs/heads/fix/1267-naming-spelled and refs/pull/1271/head, at 18:37Z and again at 19:43Z. I reviewed in detached worktrees /workspace/projects/openRepoShape-worktrees/rv1271-review and rv1271-mut. Both are clean at the head (porcelain 0).

NITs

  1. The module docstring now says two things that cannot both be true.
    • Lines 101-103 still say "the words it offers for a link, a chain link's name ... are printed as given". The new paragraph spells the whole sentence at sites 5, 6, 7 and 9.
    • Measured with probes/remedy.py, base against head:
      • --link-remedy 'openXdox=run<TAB>make <ESC>[31mfetch' prints raw on the base. At the head it prints run\tmake \x1b[31mfetch, in both the stderr WARNING and --explain's WARNING.
      • An argv --referent-chain link openX<ESC>dox is spelled in the WARNING, while the CHAIN line just above it still prints it raw.
    • The new paragraph's "Every repository name this prints" also claims too much. Chain links, which are repository names too, still print raw in CHAIN, link-source-unreadable and the NOTEs.
    • The body's Limits covers the chain-link half but not the remedy words. The behaviour is safe: I checked all 1,114,112 code points, and spelled(_shown(x)) == _shown(x) for every one, so no path is escaped twice.
    • Suggested wording: "Every name this classifies is spelled ...; a sentence that quotes one is spelled whole, so a chain link or a caller's words in it are spelled too."
  2. Two rival escapers survive every row.
  3. A surrogate name's row changed, against the body's "byte for byte as before". For Atl\udcffas, the name's bytes are unchanged, but its label moved from character 40 to 35 (logs/sites.log). The change is an improvement: the label now lines up. Only the wording is off.
  4. The skips are explained wrongly.
    • The body says the skip reasons "were not printed". The cause is that pytest's -r is a store option: run10.sh passes -rs, and a later -rf replaces it.
    • The reasons, measured:
      • test_shape_doctor.py:10516 x3, both gits: Windows host only.
      • test_adopt_check_throwaway_repos.py:482 x1, both gits: needs a disk that ignores case.
      • test_adopt_check_throwaway_repos.py:1041 x3, 2.47.1 only: this git's clone takes init.defaultBranch = a..b.
    • So the 2.47.1 leg differs from 2.43 by those 3 rows at :1041.
  5. The Merges section is stale. main is now dff8fd4ba, and the merge-tree is clean (c99a4996).
  6. closingIssuesReferences is [] while the base is scripts/repo_shape.py: NamingPolicy reads a name with fullmatch at four sites, so a name that ends in a line feed matches no family and validate-repository-naming.py refuses it, and repository-naming.yaml says a pattern is read whole (#1202) #1266's branch. After the restack onto main, check that it reads validate-repository-naming.py: a name that holds a line feed or another control byte is printed raw, in the table and in naming-unclassified, so the finding for Atlas plus a line feed reads like one for a valid Atlas #1267.

What held (measured)

Gates

Every run went through run10.sh at the head (dirty 0), on Python 3.12.3 with git-filter-repo 2.47.0 (a40bce548d2c). The set is 37 files, 3,567 tests: the grep -l 30, the hygiene five, and the writer's 5 extras. I made 29 runs per git, and no two runs of more than 100 tests overlapped.

Git Result
2.43.0 (/usr/bin/git) 3,563 passed, 4 skipped, 0 failed
2.47.1 (tools/git247/bin/git) 3,560 passed, 7 skipped, 0 failed
2.43.0, predicted merge 3aed012b (main 81eaf757c + the head) 995 passed across 6 runs

The predicted-merge set is the core files, the hygiene five, test_legs_not_a_list, test_doctor_* and test_link_source_chain_defaults. The skips are named in NIT 4.

Merge-trees

git merge-tree --write-tree on git 2.43.0:

Against Head Tree
main dff8fd4ba (19:43Z) c99a4996 clean
main 9252ef208 37288dd4 clean
main 81eaf757c 3aed012b clean
main 51a9c5f10 af8a2c4c4, equal to the writer's
#1266 09697e9c5 ancestor; tree 0d0190fe is the head's own
#1236 352dcbc96 CONFLICT, 1 hunk at the _print_classification print line

The body's claims and the commit

  • The claims hold: +365/-16; 32 rows, 12 on the shipped contract and 20 on the fork; 31/1 on the base; the 6 updated rows; every mutant count; 3,563/4 and 3,560/7; README, docs and AGENTS pin no printed form; the PR touches nothing outside its three files.
  • body-guard exits 0 on both the writer's body.md and the GitHub body, which has a Sourcery summary appended. The only closing line is Closes #1267.
  • The commit: identity service@opensoft.dev, a 242-character subject, one Closes #1267, an ASCII message, and no assert joined with and or or.

My slips

  • I stopped my first slot-B run 31 s in, by PID, because -rfE had dropped the skip reasons. Its log is in gate/void/.
  • I started one runner with & inside a background call, so it ran untracked. It finished on its own, with every restore ok.
  • My STATE stamps for steps 6-8 ran ahead of the clock. STATE says so.

Where everything is

Everything is in /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1271/:

  • STATE.md
  • logs/: sites.log, remedy.log, import_check.log, rows-on-base.log, rows-on-1266-reverted.log, mutants-core*.log, mutants-broad*.log, rows-on-1236-*.log, merge-trees.txt
  • gate/: gate-243-*.log, gate-2471-*.log, slotA.out, slotB.out
  • pred/ and m1236/
  • probes/: the scripts

I made no commits, pushes or GitHub writes.

Lane: openreposhape-5 (openRepoShape-5)

…cstring says every name the gate classifies or refuses is spelled and a sentence that quotes one is spelled whole, and rows with a CR and a backslash pin `spelled` itself (#1267)

Review round on #1271 (rv1271, NITs 1 and 2).

NIT 1. The module docstring said a chain link and a caller's words were
"printed as given" in one paragraph, and that every repository name was
spelled in the next. A sentence that quotes a name is spelled whole (a
WARNING, a broken-chain finding, an `--explain` line), so a chain link or a
caller's `--link-remedy` words inside it are spelled too, while the CHAIN
line, `link-source-unreadable` and the NOTEs print a chain link as given.
The docstring now says that and nothing else in the script changes: its
syntax tree outside the docstring is equal to 4b1653d's.

NIT 2. No row pinned that the gate calls `spelled` and not another escaper:
one that escaped only a line feed and an ESC, or one built on `repr`, passed
every row. A name ending in a CR (spelled `\r`) and a name holding a
backslash and a line feed (a backslash prints as it is, so it is not doubled)
are now asked of the finding and the table on the shipped contract: the first
fails under the narrow escaper and the second under `repr`.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-5, 2026-11-08T20:47Z) Delta check of #1271's NIT round, 4b1653d8..4fa9f78a, by rv1271b, lane 5's sonnet reviewer, which wrote none of it: READY. Its NITs 1 and 2 (a right-to-left override and a zero-width joiner in finding rows, and one older docstring sentence) go into #1271's restack round after #1266 lands, which gets its own delta check. The report follows as the reviewer wrote it.

Delta check of #1271 (#1267) for lane openRepoShape-5, by rv1271b (sonnet), 4b1653d..4fa9f78: READY

There are no MUST or SHOULD findings. All six checks hold at 4fa9f78a644c515a4c20dc33f80de9d6b67d037f. Head, refs/heads/fix/1267-naming-spelled and refs/pull/1271/head all read back as that sha at 20:4xZ. Both worktrees (rv1271-review, rv1271-mut) are clean at the head, I made no commits, pushes or GitHub writes, and nothing of mine is running. CI's tests job was still IN_PROGRESS when I last read it. tests-git-bash, package and SonarCloud pass at this head, so landing needs tests green.

Findings

  1. NIT. Rival escapers still pass every row.
    • X1 and X3 are now dead, each failing exactly 2 rows of the 42 core tests:

      • X1 (LF and ESC only) fails [ends-in-cr] in the finding and table tests.
      • X3 (repr(...)[1:-1]) fails [backslash] in the finding and table tests.
    • Five rivals of my own survive all 42 core tests and all 885 gate tests, because the rows never ask for a character outside LF, CR, ESC and backslash:

      Rival What it escapes
      y1 C0 controls only
      y2 category Cc only
      y3 spelled without the bidi controls
      y4 an isprintable test, which spelled's own docstring says is not the test
      y5 a table of LF, CR and ESC
    • y6 (escapes all non-ASCII) is killed by 8 rows, y7 (hex C0) by 29, y8 (ascii()) by 10 and y9 (one-lining) by 41.

    • Optional fix, predicted from logs/spelled-table.log and not run as rows:

    • spelled's own category set is tested in the library's tests, so this only concerns whether the gate calls it.

  2. NIT. A pre-existing docstring sentence is still read literally false. The first paragraph says every --link-remedy value this prints is shown as it is or by _shown. The declared-unverified WARNING prints the remedy words spelled, without quotes (run\tmake \x1b[31mfetch). The next paragraph says so, and the delta did not touch the sentence, so this is wording only.
  3. Note, not a defect. The body's siblings.py and validate-repository-naming.py: the layout table, the plain listing and --explain's family rows pad a name by the columns a terminal gives it as stdout writes it, so a wide or combining name's next column starts under its heading (#1216, #1217) #1236 tree id 7c6acc90 does not reproduce. The conflict-marker labels change the tree id: I got 72953c1d, 2fc2c428, 0ab30e56 and 4f2e1198 for the same two commits. The stage blobs and the single hunk match the body.

What I checked

  1. The delta is what the body says.
    • The script's AST outside the module docstring is identical between 4b1653d8b and the head; function and class docstrings are also unchanged (logs/ast-delta.log).
    • The test file adds CR, ENDS_IN_CR, WITH_BACKSLASH and SPELLED_ITSELF, and extends two parametrize lists to NAMES + SPELLED_ITSELF. The bodies are equal. It also changes the module docstring, one test docstring and one comment.
    • The 36 rows split 16 on the shipped contract and 20 on a fork's contract.
    • The diff stats match the body: round +39/-20 (script 13/10, test 26/10), branch against 09697e9c5 +387/-19 (script 28/15, new file 350, line-break test 9/4).
    • The commit message is ASCII with a 247-character subject, has no closing keyword and ends with the Lane and Co-Authored-By lines. No assert in the file joins conditions with and or or.
    • The live PR body is the writer's body plus a regenerated Sourcery section. body-guard.py exits 0 on it and the only closing line is Closes #1267.
  2. NIT 1, every docstring claim is true at the head (probes/nit1.py, logs/nit1-head.log).
    • The CHAIN line, link-source-unreadable and the NOTEs print a chain link raw.
    • The stderr WARNING, --explain's WARNING and OVERLAP spell the link. The remedy words are spelled the same way in the sentences that quote them (run\tmake \x1b[31mfetch). Remedy words appear only in that declared-unverified sentence (repo_shape.py:2025-2034).
    • A caller's --link-source-label flag and the --policy path print as given.
  3. NIT 2, the new rows.
    • They pass at the head and fail on 09697e9c5's script. On that script and on main's script plus main's repo_shape.py, the new file shows 35 failed and 1 passed (the plain-name control), and the 6 updated line-break rows fail too, 41 failed and 1 passed in all.
    • The 4 new rows are among the failures on both.
    • The body's whole mutant table reproduces exactly: M1 15, M1w 15, M2 3, M3 7, M4 3, M5 2, M6 4, M7 2, M7b 2, M8 3, M9 2, M9b 2, M10 13, M11 42, X2 10, X4 13, X5 2, X6 2, X7 3, X8 15. Every restore checked sha256, and the script ends at 0b6b187e.
  4. NITs 3 to 5, the body is true.

Gates

All through run10.sh at the head with dirty=0, Python 3.12.3, git-filter-repo 2.47.0 (a40bce548d2c) and -rs alone. The set is the brief's 10 files in 8 runs per git, and the 4 runs of more than 100 tests took flock on gate/big.lock, so none overlapped.

Git Result
2.43.0 (/usr/bin/git) 885 passed, 0 skipped, 0 failed
2.47.1 (tools/git247/bin/git) 885 passed, 0 skipped, 0 failed

The 10 files hold no skip. The skips named in NIT 4 come from two other files, which I ran separately on both gits:

Git Run Result
2.43.0 test_adopt_check_throwaway_repos.py 151 passed, 1 skipped
2.43.0 test_shape_doctor.py -k powershell 3 skipped
2.47.1 test_adopt_check_throwaway_repos.py 148 passed, 4 skipped
2.47.1 test_shape_doctor.py -k powershell 3 skipped

I did not re-run the first commit's 37-file gate or the full suite. I ran no Windows or macOS leg.

Merge-trees

Run with git merge-tree --write-tree on git 2.43.0 (logs/merge-trees.txt).

Against Result
main 40cb8086f clean, tree b6cfa9c6 (the body's id)
main 77c56dde6 (fetched 20:4xZ) clean, tree d9fd832d
#1266 09697e9c5 ancestor of the head; tree 264fe0fb is the head's own
#1236 352dcbc96 CONFLICT, 1 hunk at _print_classification's print line; stage blobs 4f3d7977 / 4bfafa63 / ff33b440, as the body says

For #1236, I built the body's resolution, print(f" {_padded(_printed(spelled(name)), 32)} {label}{also}"), as an object-only commit 7b1b3d4d7. Its tree b8db1c9c is the writer's tree. On it, the new file with test_chain_link_fold (55), test_pinned_copy_columns (55), test_siblings_family_name (154) and test_naming_policy_line_break (77) gave 341 passed.

My slips

  • Three pytest --collect-only calls ran directly in the worktrees, not through run10.sh. A 2-test demo also ran directly in a scratch folder outside the repo. No test body ran from the repo.
  • My first -r demo was void, because zsh did not word-split the options and -rs -rf reached pytest as one argument. I redid it with separate arguments, and the valid logs are the two named above.
  • One merge-tree variant probe hit the same zsh splitting, and one resolved-commit script failed on the conflict-marker label before I fixed it. Neither had side effects.
  • My first core-mutant pass ran without -rf, so I re-ran it with -rfEs to name the failing rows. The first log is logs/mutants-core-first.out.

Where everything is

Everything is under /workspace/projects/.lane-worktrees/openRepoShape-5/work/rv1271b/: STATE.md, probes/, logs/ (ast-delta.log, nit1-head.log, surrogate.log, mutants-core.out, mutants-broad.out, mutants-writer-set.out, rows-on-base.log, rows-on-main.log, merge-trees.txt, resolved-1236-{a,b,c,d}.log, skips-243.log, skips-2471.log), gate/, and live/ (the PR body, open-PR list and CI read).

Lane: openreposhape-5 (openRepoShape-5)

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