Skip to content

adopt-project.py: plan's table pads each name by the columns a terminal gives it, not the code points it holds, so a wide or combining name's leg column lines up under leg with every other row's (#1034) - #1178

Draft
brettheap wants to merge 2 commits into
mainfrom
l3-1034-plan-table-display-width

Conversation

@brettheap

@brettheap brettheap commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What changed

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

plan's table padded each name with :<34, which counts code points, not the columns a terminal gives them. On a UTF-8 stdout the name U+540D U+524D .md (two wide ideographs) takes 7 columns and was padded as 5, so its leg began at column 37, 2 right of the heading's 35. The name e U+0301 .md (a combining acute) takes 4 columns and was padded as 5, so its leg began at 34. Names that stdout escapes have lined up since #900's PR (#1030).

adopt-project.py:

  • _columns(text) counts the columns a terminal gives a text: none for a character whose general category is in ZERO_COLUMN_CATEGORIES (Mn, Me and Cf: a non-spacing mark, an enclosing mark, a format character) or that is a Hangul medial vowel or final consonant (MEDIAL_AND_FINAL_JAMO, U+1160 to U+11FF, and MEDIAL_AND_FINAL_JAMO_EXTENDED_B, U+D7B0 to U+D7FF), two for one unicodedata.east_asian_width calls wide or fullwidth (TWO_COLUMN_WIDTHS, W and F), and one for any other.
  • _padded(text, columns) pads a text to that many columns, as f"{text:<{columns}}" pads it to that many code points. A wider text is left as it is, as there.
  • _print_entries pads _printed(_shown(entry.path)) with _padded(..., 34) in place of :<34. The text measured is the one printed, after _shown and _printed, so an ASCII stdout's escapes still count one column each (adopt-project.py: a kept or dropped name reaches the split commit and the pull request body unspelled, and _spelled, _shown and plan's report print text that reads as another name, or raw #900).

tests/test_adopt_spelled_names.py:

  • test_a_name_takes_the_columns_a_terminal_gives_it: 14 names whose columns are counted by hand: ASCII, wide, fullwidth, halfwidth, ambiguous, a combining acute, a Thai mark of combining class 0, a Devanagari spacing mark, the wide voiced sound mark, a Hangul syllable spelled in its three jamo, one spelled with jamo of Hangul Jamo Extended-B, an enclosing mark, a zero-width space, and a name as an ASCII stdout escapes it. glibc's wcswidth gives each new name the count the row holds.
  • test_a_name_wider_than_its_column_is_padded_by_no_space: ten of U+540D U+524D and .md, 43 columns, is _padded to 34 as it is.
  • test_the_table_pads_a_wide_and_a_combining_name_by_their_columns: _print_entries on a UTF-8 stdout over five names. Every row's leg begins at column 35, under leg.
  • test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading: the issue's table. plan runs end to end with a UTF-8 stdout over a source holding the two wide names, the combining one and U+1112 U+1161 U+11AB .md. Each of the four is asserted among the rows, and every row's leg at 35.

Fix round

RV1178's independent review of fa56168a (issuecomment-6351653129). This round is one new commit, c4beba19, on top; nothing was amended or force-pushed.

Decided where the Direction left it open

  • A zero-column character is one of general category Mn, Me or Cf, not one unicodedata.combining calls combining. The canonical combining class is 0 for marks a terminal still draws over the character before them: the Thai vowel sign U+0E35 (Mn), the enclosing circle U+20DD (Me) and the variation selector U+FE0F (Mn) among them. Mutant M9 asks combining (or Cf) in place of the category, and the Thai-mark and enclosing-mark rows kill it.
  • The category is asked before the width. The voiced sound mark U+3099, which a decomposed U+304C ends in, is Mn and East Asian wide. A terminal draws it over the kana before it. M8 asks the width first, and the wide-voiced-mark row kills it.
  • East Asian ambiguous (A) and halfwidth (H) count one column each, as a terminal outside an East Asian locale draws them (M10 and M11). A spacing mark (Mc) counts one too, as glibc counts it (M15).
  • Cf is in the set although _shown spells every format character (str.isprintable is false for each), so none reaches the table as it is today. The set says what a terminal does, and the zero-width-space row holds _columns to it (M5).
  • The Hangul sets are glibc's two ranges, whole. U+D7B0 to U+D7FF holds 8 unassigned code points (U+D7C7 to U+D7CA and U+D7FC to U+D7FF). str.isprintable is false for each, so _shown spells them and none reaches _columns as it is.

Left out of scope

  • East Asian ambiguous characters count one column. A terminal set to draw them wide, as in an East Asian locale, puts such a row's leg right of its heading. plan cannot learn from stdout which way the terminal draws them.
  • 72 printable code points that glibc draws two columns wide count one here, as on main. A sweep of every printable code point under Python 3.12.3's Unicode 15.0.0 compares _columns with glibc's wcwidth (C.utf8). They differ only on the 64 Yijing hexagram symbols U+4DC0 to U+4DFF (So, East Asian width N) and the 8 code points U+3248 to U+324F (No, width A). Before this round the 232 assigned jamo of the two Hangul ranges differed too. No code point counts differently from glibc where main's one column agrees with it.
  • check's names table ({name:<28}) prints the three names Plan.names() returns. Plan.names() runs each through checked_value, whose SAFE_ARG_RE (^[A-Za-z0-9._/@+~-]{1,255}$) holds it to ASCII, so each character takes one column. It is unchanged.
  • Other tables that pad by code points, which RV1178 lists as follow-ups to file on Brett Heap's word: templates/family-root/scripts/siblings.py:729 and scripts/validate-repository-naming.py:1297. They are other files.
  • The issue has no "Needs a ruling" or "Documented limit" section.

Measured

A scratch probe runs plan with PYTHONIOENCODING=utf-8 over a source holding README.md, src/app.py, U+540D U+524D .md, e U+0301 .md and U+FF21 U+FF22 .md. It measures each row's leg column on its own, with unicodedata (W and F two, Mn, Me and Cf none, any other one). The heading's leg is at column 35. Only the probe's ASCII lines are quoted: each name is given in ascii() form.

Before, at 961640c6:

exit 0
|path                               leg        conf     files rule|
    name='path' code points=4 columns=4 -> leg column=35
|README.md                          root       high         1 root-front-door|
    name='README.md' code points=9 columns=9 -> leg column=35
    name='e\u0301.md' code points=5 columns=4 -> leg column=34
|src/                               code       high         1 code-source-and-tests|
    name='src/' code points=4 columns=4 -> leg column=35
    name='\u540d\u524d.md' code points=5 columns=7 -> leg column=37
    name='\uff21\uff22.md' code points=5 columns=7 -> leg column=37

After, at fa56168a:

exit 0
|path                               leg        conf     files rule|
    name='path' code points=4 columns=4 -> leg column=35
|README.md                          root       high         1 root-front-door|
    name='README.md' code points=9 columns=9 -> leg column=35
    name='e\u0301.md' code points=5 columns=4 -> leg column=35
|src/                               code       high         1 code-source-and-tests|
    name='src/' code points=4 columns=4 -> leg column=35
    name='\u540d\u524d.md' code points=5 columns=7 -> leg column=35
    name='\uff21\uff22.md' code points=5 columns=7 -> leg column=35

Fix round. RV1178's probe, copied into this writer's scratch, runs plan over a source of 13 names, and measures each row's leg with glibc's wcswidth (C.utf8), not with _columns. The UTF-8 stdout's rows at main, at fa56168a and at this head:

== main-961640c6 stdout=utf-8 exit=0
  leg@35  name='path' codepoints=4 wcswidth=4
  leg@35  name='README.md' codepoints=9 wcswidth=9
  leg@35  name='`caf\\udce9.md`' codepoints=14 wcswidth=14
  leg@34  name='e\u0301.md' codepoints=5 wcswidth=4
  leg@35  name='src/' codepoints=4 wcswidth=4
  leg@35  name='\xe9.md' codepoints=4 wcswidth=4
  leg@34  name='\u1112\u1161\u11ab.md' codepoints=6 wcswidth=5
  leg@34  name='\u263a\ufe0f.md' codepoints=5 wcswidth=4
  leg@37  name='\u540d\u524d.md' codepoints=5 wcswidth=7
  leg@55  name='\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d.md' codepoints=23 wcswidth=43
  leg@37  name='\uff21\uff22.md' codepoints=5 wcswidth=7
  leg@37  name='`\U0001f468\\u200d\U0001f469.md`' codepoints=13 wcswidth=15
  leg@36  name='\U0001f600.md' codepoints=4 wcswidth=5
== head-fa56168a stdout=utf-8 exit=0
  leg@35  name='path' codepoints=4 wcswidth=4
  leg@35  name='README.md' codepoints=9 wcswidth=9
  leg@35  name='`caf\\udce9.md`' codepoints=14 wcswidth=14
  leg@35  name='e\u0301.md' codepoints=5 wcswidth=4
  leg@35  name='src/' codepoints=4 wcswidth=4
  leg@35  name='\xe9.md' codepoints=4 wcswidth=4
  leg@33  name='\u1112\u1161\u11ab.md' codepoints=6 wcswidth=5
  leg@35  name='\u263a\ufe0f.md' codepoints=5 wcswidth=4
  leg@35  name='\u540d\u524d.md' codepoints=5 wcswidth=7
  leg@44  name='\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d.md' codepoints=23 wcswidth=43
  leg@35  name='\uff21\uff22.md' codepoints=5 wcswidth=7
  leg@35  name='`\U0001f468\\u200d\U0001f469.md`' codepoints=13 wcswidth=15
  leg@35  name='\U0001f600.md' codepoints=4 wcswidth=5
== fix-round stdout=utf-8 exit=0
  leg@35  name='path' codepoints=4 wcswidth=4
  leg@35  name='README.md' codepoints=9 wcswidth=9
  leg@35  name='`caf\\udce9.md`' codepoints=14 wcswidth=14
  leg@35  name='e\u0301.md' codepoints=5 wcswidth=4
  leg@35  name='src/' codepoints=4 wcswidth=4
  leg@35  name='\xe9.md' codepoints=4 wcswidth=4
  leg@35  name='\u1112\u1161\u11ab.md' codepoints=6 wcswidth=5
  leg@35  name='\u263a\ufe0f.md' codepoints=5 wcswidth=4
  leg@35  name='\u540d\u524d.md' codepoints=5 wcswidth=7
  leg@44  name='\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d\u540d\u524d.md' codepoints=23 wcswidth=43
  leg@35  name='\uff21\uff22.md' codepoints=5 wcswidth=7
  leg@35  name='`\U0001f468\\u200d\U0001f469.md`' codepoints=13 wcswidth=15
  leg@35  name='\U0001f600.md' codepoints=4 wcswidth=5

At this head every row's leg is at 35 on a UTF-8, an ASCII and a Latin-1 stdout, but the 43-column name's, which no pad can bring back: it is followed by the one space that ends every name.

Tests

At c4beba19, each file runs alone, on Python 3.12.3, as timeout 2400 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed <file>. A stub gh that exits 97 is first on PATH, with GH_TOKEN=bogus, an empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, GIT_CONFIG_GLOBAL=/dev/null and EMAIL set.

file result
tests/test_adopt_spelled_names.py 44 passed in 13.99s
tests/test_shape_doctor.py 445 passed, 3 skipped in 653.88s (0:10:53)
tests/test_scaffold_leg_path_collisions.py 104 passed in 7.48s
tests/test_repo_hygiene.py 101 passed in 4.37s
tests/test_no_auto_maintenance.py 58 passed, 2 skipped in 13.49s
tests/test_path_guard_hygiene.py 49 passed in 14.87s
tests/test_agents_md_codes.py 47 passed in 2.51s
tests/test_cli_reference.py 23 passed in 40.76s
tests/test_update_add_codes_documented.py 6 passed in 0.56s

The 2 skips in tests/test_no_auto_maintenance.py are its case for a session that began with GIT_CONFIG_GLOBAL set, which the stub environment sets.

Mutants

M0, origin/main's adopt-project.py under the rows (tests/test_adopt_spelled_names.py -k "columns or keeps_each_column_under_its_heading or wider_than_its_column"): 17 failed, 1 passed, 26 deselected in 2.15s. The one that passes is #900's test_plan_under_the_c_locale_keeps_each_column_under_its_heading. Failed:

  • test_a_name_takes_the_columns_a_terminal_gives_it[ascii]
  • test_a_name_takes_the_columns_a_terminal_gives_it[wide]
  • test_a_name_takes_the_columns_a_terminal_gives_it[fullwidth]
  • test_a_name_takes_the_columns_a_terminal_gives_it[halfwidth]
  • test_a_name_takes_the_columns_a_terminal_gives_it[ambiguous]
  • test_a_name_takes_the_columns_a_terminal_gives_it[combining-acute]
  • test_a_name_takes_the_columns_a_terminal_gives_it[thai-mark-of-class-0]
  • test_a_name_takes_the_columns_a_terminal_gives_it[spacing-mark]
  • test_a_name_takes_the_columns_a_terminal_gives_it[wide-voiced-mark]
  • test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo]
  • test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo-extended-b]
  • test_a_name_takes_the_columns_a_terminal_gives_it[enclosing-mark]
  • test_a_name_takes_the_columns_a_terminal_gives_it[zero-width-space]
  • test_a_name_takes_the_columns_a_terminal_gives_it[as-an-ascii-stdout-escapes-it]
  • test_a_name_wider_than_its_column_is_padded_by_no_space
  • test_the_table_pads_a_wide_and_a_combining_name_by_their_columns
  • test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading

M1 to M17 each break one behaviour of adopt-project.py, run the whole of tests/test_adopt_spelled_names.py, then restore the file from a copy saved before the first and check it with cmp. M1 to M11 are the first round's, their patterns moved to the new _columns; M12 to M17 are the fix round's.

mutant what it breaks verdict killed by restored
M1-pad-by-code-points main's :<34 back: the name padded by its code points KILLED test_the_table_pads_a_wide_and_a_combining_name_by_their_columns, test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading identical
M2-measure-before-stdout-escapes _printed(_padded(_shown(...), 34)): measured before stdout's escapes KILLED test_the_table_pads_a_name_as_an_ascii_stdout_prints_it, test_plan_under_the_c_locale_keeps_each_column_under_its_heading identical
M3-no-Mn Mn left out of ZERO_COLUMN_CATEGORIES KILLED test_a_name_takes_the_columns_a_terminal_gives_it[combining-acute], test_a_name_takes_the_columns_a_terminal_gives_it[thai-mark-of-class-0], test_a_name_takes_the_columns_a_terminal_gives_it[wide-voiced-mark], test_the_table_pads_a_wide_and_a_combining_name_by_their_columns, test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading identical
M4-no-Me Me left out of ZERO_COLUMN_CATEGORIES KILLED test_a_name_takes_the_columns_a_terminal_gives_it[enclosing-mark] identical
M5-no-Cf Cf left out of ZERO_COLUMN_CATEGORIES KILLED test_a_name_takes_the_columns_a_terminal_gives_it[zero-width-space] identical
M6-no-W W left out of TWO_COLUMN_WIDTHS KILLED test_a_name_takes_the_columns_a_terminal_gives_it[wide], test_a_name_takes_the_columns_a_terminal_gives_it[wide-voiced-mark], test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo], test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo-extended-b], test_a_name_wider_than_its_column_is_padded_by_no_space, test_the_table_pads_a_wide_and_a_combining_name_by_their_columns, test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading identical
M7-no-F F left out of TWO_COLUMN_WIDTHS KILLED test_a_name_takes_the_columns_a_terminal_gives_it[fullwidth], test_the_table_pads_a_wide_and_a_combining_name_by_their_columns, test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading identical
M8-width-asked-before-category the width asked before the category KILLED test_a_name_takes_the_columns_a_terminal_gives_it[wide-voiced-mark], test_the_table_pads_a_wide_and_a_combining_name_by_their_columns identical
M9-combining-class-not-category unicodedata.combining (or Cf) in place of the category KILLED test_a_name_takes_the_columns_a_terminal_gives_it[thai-mark-of-class-0], test_a_name_takes_the_columns_a_terminal_gives_it[enclosing-mark] identical
M10-ambiguous-as-two East Asian ambiguous (A) counted two KILLED test_a_name_takes_the_columns_a_terminal_gives_it[ambiguous] identical
M11-halfwidth-as-two halfwidth (H) counted two KILLED test_a_name_takes_the_columns_a_terminal_gives_it[halfwidth] identical
M12-no-medial-and-final-jamo fix round: neither Hangul set asked (S1) KILLED test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo], test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo-extended-b], test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading identical
M13-pad-at-least-one-space fix round: _padded pads max(1, ...) spaces (S2) KILLED test_a_name_wider_than_its_column_is_padded_by_no_space identical
M14-pad-by-the-overrun fix round: _padded pads abs(...) spaces (S2) KILLED test_a_name_wider_than_its_column_is_padded_by_no_space identical
M15-Mc-as-zero fix round: Mc added to ZERO_COLUMN_CATEGORIES (the Mc NIT) KILLED test_a_name_takes_the_columns_a_terminal_gives_it[spacing-mark] identical
M16-no-hangul-jamo-block fix round: MEDIAL_AND_FINAL_JAMO not asked KILLED test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo], test_plan_on_a_utf_8_stdout_keeps_each_column_under_its_heading identical
M17-no-hangul-jamo-extended-b fix round: MEDIAL_AND_FINAL_JAMO_EXTENDED_B not asked KILLED test_a_name_takes_the_columns_a_terminal_gives_it[conjoining-jamo-extended-b] identical

Merges

git merge-tree --write-tree of c4beba19 with origin/main (961640c6) and with each open PR that touches adopt-project.py, at the heads GitHub listed live in this round. No open PR touches tests/test_adopt_spelled_names.py.

origin/main rc=0 tree=3a73d22fc8036ea71c0d27f9a082619f10004d9f head-tree=3a73d22fc8036ea71c0d27f9a082619f10004d9f
#1105 rc=0 clean tree=1c6a15950dcad07d0bda08fb51dbd3c6ed5fe6a8
#1106 rc=0 clean tree=4929131fd0712f17f2d1713d9d71dd92772535c0
#1118 rc=0 clean tree=651f4f7fdd152817aa24a40f0f1e4dba16470044
#1119 rc=0 clean tree=75ba85a18d5c04703a72d59fc3b3835c50e7deea
#1120 rc=0 clean tree=586e7ca556d6292485777852fe56fda139c214a7
#1121 rc=0 clean tree=bfaab544f826403d83672f107d55a1e5ac038dd7
#1122 rc=0 clean tree=f316bf6e26307a1753358dc4b6096c3407a119e8
#1123 rc=0 clean tree=666afa4b8587f57fb12a222bf34a85e5c58dc561
#1125 rc=0 clean tree=5a7adf628ad225f1d459fa0afc6b4baa84ecfd99
#1141 rc=0 clean tree=d0af1eedbdbc0742bbf3657538abca67050ad4e0
#1143 rc=0 clean tree=361e6b4d4a18f76e81670545a969885352cdf3c4
#1153 rc=0 clean tree=6618e42a0b1bf289d5d60295621259d20215e8f3
#1154 rc=0 clean tree=319b3f7e9fd6e64c3f9a22eb1c41abf7d7ff4c78
#1169 rc=0 clean tree=2fe110322652407bb36a45a69b52334c87feb8cd
#1170 rc=0 clean tree=42aa2210f5a8adda2bc223ca284b0760be668b0c
#1171 rc=0 clean tree=c4bf31cd60669d8be6c945932e34b43792790b1c
#1177 rc=0 clean tree=cf87b304c390bcd6a2418ffbd3001f8ef278f541
#1180 rc=0 clean tree=b24f0aa85f3392bbbc26606959e2bccee2e2d9ce
#1186 rc=0 clean tree=686d1bd1c6652fda5eca6f7e7d0730f384d3bddb

All are clean. #1171 has _shown spell a default-ignorable character as its code-point escape, U+1160 among them, and adds a repo_shape import on a line of its own. The table here pads the text _shown and _printed return, whatever it is, so a name #1171 spells is measured as its ASCII escape, one column to each character. No PR adds import unicodedata, or a _columns, _padded, ZERO_COLUMN_CATEGORIES, TWO_COLUMN_WIDTHS, MEDIAL_AND_FINAL_JAMO or MEDIAL_AND_FINAL_JAMO_EXTENDED_B of its own, or changes _print_entries.

Closes #1034

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

…inal gives it, not the code points it holds, so a wide or combining name's `leg` column lines up under `leg` with every other row's (#1034)

`_print_entries` padded the name it prints with `:<34`, which counts code
points. On a UTF-8 stdout a name of two wide ideographs plus `.md` takes 7
columns and was padded as 5, so its row's `leg` began at column 37, 2 right
of the heading's 35; `e` plus a combining acute plus `.md` takes 4 columns
and was padded as 5, so its `leg` began at 34.

The name is now padded by `_padded`, which measures it with `_columns`:
two columns for a character `unicodedata.east_asian_width` calls wide or
fullwidth, none for a non-spacing mark, an enclosing mark or a format
character (general category Mn, Me or Cf), and one for any other. The mark
is asked first, so the wide voiced sound mark U+3099 takes none. The text
measured is the one printed, after `_shown` and `_printed`, so an ASCII
stdout's escapes are still counted one column each (#900).

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

sourcery-ai Bot commented Oct 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes plan's entries table align by terminal display columns rather than Unicode code-point counts, handling wide, combining, enclosing, and format characters while measuring the text actually printed; comprehensive unit, table, and end-to-end tests cover the Unicode rules and UTF-8 output.

Sequence diagram for terminal-aware plan table padding

sequenceDiagram
    participant Plan
    participant EntryPath
    participant Shown as _shown
    participant Printed as _printed
    participant Padded as _padded
    participant Columns as _columns
    participant Terminal
    Plan->>EntryPath: read entry.path
    Plan->>Shown: _shown(entry.path)
    Shown-->>Plan: displayed text
    Plan->>Printed: _printed(displayed text)
    Printed-->>Plan: text actually printed
    Plan->>Padded: _padded(printed text, 34)
    Padded->>Columns: _columns(text)
    Columns->>Terminal: category and east_asian_width
    Terminal-->>Columns: 0, 1, or 2 columns
    Columns-->>Padded: display width
    Padded-->>Plan: text plus column-based spaces
    Plan-->>Terminal: print aligned entry row
Loading

File-Level Changes

Change Details Files
Replace code-point-based table padding with terminal-column-aware width calculation for Unicode names.
  • Add _columns using Unicode general categories and East Asian widths, prioritizing zero-width categories.
  • Add _padded to append spaces based on display columns while preserving overlong text.
  • Measure the fully transformed text after _shown and _printed, preserving escaped-output alignment behavior.
  • Use column-aware padding for the plan entries table.
adopt-project.py
Add focused Unicode alignment coverage for width calculation and end-to-end table output.
  • Test ASCII, wide, fullwidth, halfwidth, ambiguous, combining, enclosing, format, and escaped characters.
  • Verify wide and combining names place every leg field under its heading.
  • Exercise plan end to end with UTF-8 paths and assert consistent column placement.
tests/test_adopt_spelled_names.py

Assessment against linked issues

Issue Objective Addressed Explanation
#1034 Pad each name in plan's table according to terminal display width rather than Unicode code-point count, so wide and combining names’ leg columns align with the heading. ✅
#1034 Add tests covering wide and combining names and asserting that every row’s leg column starts at the same column. ✅

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-22T21:25:19Z) Independent review of #1178 at fa56168a (RV1178, opus).

Verdict: NOT READY, on the body. The code is sound. CI tests was still running at 21:12Z.

MUST

  • Body: a "Summary by Sourcery" block follows the robot line. Strip it. Lane 3 strips it now.

SHOULD

NIT

Verified:

  • My own probe (glibc wcswidth) puts every row's leg at 35 on UTF-8, ASCII and Latin-1 stdout, except the Hangul row and a name past 34 columns.
  • The test rows' characters are the same in Unicode 15.0.0 (Python 3.12) and 16.0.0 (Python 3.14).
  • Mutants M1, M3, M6, M8 and M9 are killed.
  • Tests: spelled names 40, path guard 49, repo hygiene 101, collisions 104, and doctor 445 passed with 3 skipped.
  • All 18 open PRs touching adopt-project.py merge clean.
  • Sonar: gate OK, duplication 0.0%.

Follow-ups, to file on Brett Heap's word:

  • templates/family-root/scripts/siblings.py:729: the layout table pads a folder name by code points.
  • scripts/validate-repository-naming.py:1297: pads names from the command line by code points.

Lane: openreposhape-3 (openRepoShape-3)

… consonant no column, as glibc's `wcwidth` does, so a syllable spelled in its jamo has its `leg` under `leg`, and the rows hold a name wider than 34 columns and a spacing mark (#1034)

The fix round of RV1178's review of fa56168.

S1: `_columns` counted the medial vowels and final consonants U+1160 to
U+11FF and U+D7B0 to U+D7FF one column each, as their category is Lo and
their East Asian width N. A terminal draws them inside the syllable, and
glibc's `wcwidth` gives them none, so `plan` put the `leg` of U+1112
U+1161 U+11AB `.md` at column 33, where main's `:<34` put it at 34. The
two ranges are now `MEDIAL_AND_FINAL_JAMO` and
`MEDIAL_AND_FINAL_JAMO_EXTENDED_B`, in `NOT_UTF8_BYTES`'s idiom, and
`_columns` counts a character of either as none. The name takes 5
columns, and its `leg` is at 35: a `_columns` row for it, one for a
syllable in the jamo of Hangul Jamo Extended-B, and the name in `plan`'s
row.

S2: no row held `_padded` on a name wider than its columns. Ten of
U+540D U+524D and `.md` take 43 columns, and `_padded(name, 34)` is the
name as it is.

NITs: a spacing-mark row, U+0915 U+093F `.md`, 5 columns; and `plan`'s
row asserts that `e` U+0301 `.md` is among its rows.

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-22T23:55:40Z) Delta review of #1178 at c4beba19 (RV1178, opus): READY.

Every finding from the first round is fixed at this head:

  • S1: U+1160-U+11FF and U+D7B0-U+D7FF now count 0. My plan probe (glibc wcswidth) puts U+1112 U+1161 U+11AB .md at leg 35 (fa56168a: 33, main: 34). 36 of the 39 rows are at 35 on UTF-8, ASCII and Latin-1 stdout; the other three are the 43-column name. Against glibc over every printable code point, only the 72 symbols glibc draws wide (U+4DC0-U+4DFF, U+3248-U+324F) still differ, and main got those wrong too.
  • S2: test_a_name_wider_than_its_column_is_padded_by_no_space kills max(1, ...) and abs(...).
  • Mc NIT: the spacing-mark row kills the mutant that counts Mc as 0.
  • Line 626 NIT: e U+0301 .md and the Hangul name are now asserted among the rows.
  • Body NITs:
    • The checked_value sentence is right.
    • The 19 PRs were re-listed live and merge-tree run again: all clean, and every tree hash matches the body's.
    • The body ends Closes #1034, the Lane line, then the robot line, with nothing after it. It is still a draft with no milestone.

What the commit adds:

  • The two constants cover exactly the 232 code points glibc gives no column. The unassigned U+D7C7-U+D7CA and U+D7FC-U+D7FF they also take in are spelled by _shown before they are measured.
  • U+0915, U+093F, U+1100, U+D7B0 and U+D7CB keep their category and East Asian width in Unicode 15.0.0 (Python 3.12) and 16.0.0 (3.14.7).
  • The commit is by Brett Heap <service@opensoft.dev>, is all ASCII, and ends with Co-Authored-By.

Checks:

  • Tests: tests/test_adopt_spelled_names.py 44 passed. The doctor was skipped here: it is green on the bench (445 passed, 3 skipped) and in CI.
  • Mutants, all KILLED, each restored and cmp identical: M1, max(1, ...), abs(...), Mc as 0, no jamo sets, no Extended-B set and no basic jamo set.
  • CI: tests, tests-git-bash, package and SonarCloud all SUCCESS on c4beba19.

Before landing, read the body's ending back once more, in case a bot appends a block again.

Lane: openreposhape-3 (openRepoShape-3)

This was referenced Oct 23, 2026
@brettheap brettheap added this to the v1.2.1 milestone Oct 23, 2026

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.

adopt-project.py: plan's table pads a name by code points, not by display width, so a wide or combining name's leg column stands out of line

1 participant