Repository navigation
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
brettheap wants to merge 1 commit into
Conversation
…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) `templates/family-root/scripts/siblings.py`'s `Sibling.row` padded the member's name with `:<18`, and `scripts/validate-repository-naming.py`'s plain listing padded the target's name with `:<32`, its `--explain` rows a policy's family id with `:<18`. A format width counts code points. On main 50ce8ab, measured: - `make siblings --dry-run`: the `state` of a member named with three wide characters began at column 34, and of one with a combining acute at 30, where every ASCII row's begins at 31. On an ASCII stdout, which writes each as its escapes, 46 and 36. - `validate-repository-naming.py codexGlass <wide> <combining>`: the labels began at 38 and 34 beside `codexGlass`'s 35; on an ASCII stdout 50 and 40. - `--explain` under a policy whose family id is wide or combining: the pattern began 3 right and 1 left on a UTF-8 stdout, 15 and 5 right on an ASCII one. Each pads by display columns now, by the rule #1034 gives `adopt-project.py`'s `plan` table (`ZERO_COLUMN_CATEGORIES`, `TWO_COLUMN_WIDTHS`, the two Hangul jamo sets), counted over the text as stdout writes it. An ASCII name prints byte for byte as before, a name wider than its field gets no space and is not cut, and a family id that YAML reads as a number or a boolean is padded by `format` as it was. The rule is written in each file, not in `scripts/repo_shape.py`: both are pinned copies that may run beside an older kept `repo_shape.py`, so neither may import a name new to it (the skew rule of tests/test_pinned_copy_text.py, and siblings.py's own comment on `shell_line`). tests/test_pinned_copy_columns.py asks both copies the same questions and holds them to one answer for every code point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR updates the pinned siblings and repository-naming scripts to pad displayed text by the terminal columns occupied after stdout encoding, fixing alignment for wide, combining, and escaped Unicode names while retaining existing behavior for ASCII and non-text IDs. Comprehensive tests cover both output paths, all Unicode-width rules, stdout handlers, pinned-copy parity, and regression mutants. Sequence diagram for aligned siblings table outputsequenceDiagram
participant Sibling
participant Stdout
participant Terminal
Sibling->>Stdout: spelled(project)
Stdout-->>Sibling: _printed(text)
Sibling->>Sibling: _padded(text, 18)
Sibling->>Stdout: row with state and branch
Stdout->>Terminal: write encoded row
Terminal-->>Stdout: columns align under state heading
Sequence diagram for repository naming classification outputsequenceDiagram
participant Validator
participant Stdout
participant Terminal
Validator->>Stdout: _printed(name)
Stdout-->>Validator: text as stdout writes it
Validator->>Validator: _padded(text, 32)
Validator->>Stdout: _print_classification(name, found)
Stdout->>Terminal: write classification row
Terminal-->>Stdout: label aligns under its heading
Flow diagram for terminal-column-aware name paddingflowchart LR
Input[Name or family id] --> Printed[_printed: encode with stdout handler]
Printed --> TextCheck{Text value?}
TextCheck -->|yes| Columns[_columns: count terminal columns]
Columns --> Padded[_padded: add spaces to target width]
TextCheck -->|no family id| Format[Format with existing width]
Padded --> Output[Aligned table row]
Format --> Output
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
(lane openRepoShape-3, 2026-10-23T21:51:12Z) RV1236 independent review of #1236 at Rule: the three copies give one answer for all 1,114,112 code points (0 differ, unicodedata 15.0.0): Measured with glibc
Imports: only Tests at the head, in one run of the PR's files, Mutants, RV1236's own script, each KILLED, restored and
Jobserver: Merges, re-listed live (50 open): merge-tree is clean with main Sonar: gate OK, 0 issues, 0.0% duplication. CI at 21:50Z: NITs:
Follow-up: when #1178 lands, the parity row ( #1236 lands under Brett Heap's v1.2.1 merge word once Lane: openreposhape-3 (openRepoShape-3) |



What changed
A v1.2.1 follow-up from lane 3's independent reviews (#1216, #1217). It lands under Brett Heap's v1.2.1 merge word (2026-10-23T13:10:26Z) once reviewed and green.
Three tables padded a person's text with a format width, which counts code points, not the columns a terminal gives them:
templates/family-root/scripts/siblings.py's layout table (make siblings) and the dispatch's(b) what ran(make park,make resume), both throughSibling.row's:<18(templates/family-root/scripts/siblings.py: the layout table pads each member's name by code points, not by the columns a terminal gives it, so a wide or combining name'sstatecolumn stands out of line (the family-root twin of #1034) #1216);scripts/validate-repository-naming.py's plain listing,:<32(scripts/validate-repository-naming.py: the plain listing pads each name from the command line or stdin by code points, not by the columns a terminal gives it, so a wide or combining name's label column stands out of line (twin of #1034) #1217);--explainfamily rows,:<18over a policy's family id (_no_family_linesand_family_row, main's:795and:919, which scripts/validate-repository-naming.py: the plain listing pads each name from the command line or stdin by code points, not by the columns a terminal gives it, so a wide or combining name's label column stands out of line (twin of #1034) #1217's Direction asked to be read before they were left).At main, a name of the three wide characters U+65E5 U+672C U+8A9E stood 3 columns right of every other row, and
eU+0301cole(a combining acute) 1 column left. On an ASCII stdout, which writes each as its escapes, they stood 15 and 5 columns right (Measured).templates/family-root/scripts/siblings.py(#1216):ZERO_COLUMN_CATEGORIES,TWO_COLUMN_WIDTHS,MEDIAL_AND_FINAL_JAMO,MEDIAL_AND_FINAL_JAMO_EXTENDED_B,_char_columnsand_columnsare the rule 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'slegcolumn lines up underlegwith every other row's (#1034) #1178 givesadopt-project.py'splantable (adopt-project.py:plan's table pads a name by code points, not by display width, so a wide or combining name'slegcolumn stands out of line #1034, atc4beba19), unchanged:_padded(text, columns)pads to that many columns, as:<Npads to N code points. A wider text is left whole, with no space added._printed(text)is the text as stdout will write it, in stdout's encoding and with its handler, asadopt-project.py's_printedhas it (adopt-project.py: a kept or dropped name reaches the split commit and the pull request body unspelled, and_spelled,_shownandplan's report print text that reads as another name, or raw #900). On an ASCII stdout each escape counts one column per character.Sibling.rowpads_printed(spelled(self.project))with_padded(..., 18).kindandstatekeep:<9and:<22: both are this file's own ASCII words, and the<target>in amake <target>state is held toMAKE_TARGET_RE.import unicodedata, from the standard library, asosandreare imported.scripts/validate-repository-naming.py(#1217):_print_classificationpads_printed(name)with_padded(..., 32)._family_id_padded(family_id)serves_no_family_linesand_family_row.NamingPolicy.__init__(scripts/repo_shape.py) checks nothing of an id, and--policytakes any file. So a policy can name a family U+65E5 U+672C U+8A9E, and its--explainrow stood 3 columns right at main.5,true) reaches_family_rowtoo, and printed throughformat(5, and1fortrue). It still does, byte for byte.An ASCII name prints byte for byte as before in every table. The existing rows that pin those bytes pass unchanged:
test_a_plain_holder_prints_exactly_what_it_printed_before, andtests/test_chain_link_fold.py'scodexGlassrows.Every family holder will see an
upstream-changedrow forscripts/siblings.py, and every assembly root one forscripts/validate-repository-naming.py, as #1148 says of a pinned copy whose bytes move.scripts/repo_shape.pyis unchanged, so no root or holder sees a row for it.Where the rule lives, and why
In each of the two files, and not in
scripts/repo_shape.py.scripts/repo_shape.py, whichupdate-shape.py apply --accept-localcan keep at an older version.make siblings,make parkandmake resume, assiblings.py's comment at itsmvlines says ofshell_line, and it would stop the naming gate. Since update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536,update-accept-local-missing-namerefuses such an apply instead.tests/test_pinned_copy_text.pyholds every pinned importer to this rule, andvalidate-repository-naming.pyalready writes its own_shownfor the same reason.scripts/repo_shape.pywould put anupstream-changed scripts/repo_shape.pyrow in front of every project and every holder. The holders' row would be for a function theirsiblings.pycould not import.scripts/shape_materialize.pynames that cost at each helper it keeps out of the library.tests/test_pinned_copy_columns.pyasks both copies the same 14 hand-counted questions: 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'slegcolumn lines up underlegwith every other row's (#1034) #1178's table, with these issues' two names. It also holds the two copies to one answer for each of the 1,114,112 code points.adopt-project.py's copy is 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'slegcolumn lines up underlegwith every other row's (#1034) #1178's, and this PR leavesadopt-project.pyalone.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'slegcolumn lines up underlegwith every other row's (#1034) #1178 lands, the parity row can takeadopt-project.pyas a third copy. That row is not added here, because main'sadopt-project.pyhas no_columns.ifstatements. The per-character count is written as_char_columns, with twoifstatements, where 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'slegcolumn lines up underlegwith every other row's (#1034) #1178 nests one conditional expression in another (python:S3358, which_print_classification's own comment names). The table and the parity row hold the two forms to the same answers.Decided where the Direction left it open
--explain's two id rows are changed, because a policy can name an id outside ASCII (scripts/validate-repository-naming.py: the plain listing pads each name from the command line or stdin by code points, not by the columns a terminal gives it, so a wide or combining name's label column stands out of line (twin of #1034) #1217's Direction asked for that to be read before they were left). Ids that are not text keepformat.siblings.py's row. Every other padded field there is the file's own ASCII.layout()intests/test_siblings_family_name.pycollapses the pad, so the new rows parse the raw lines, as templates/family-root/scripts/siblings.py: the layout table pads each member's name by code points, not by the columns a terminal gives it, so a wide or combining name'sstatecolumn stands out of line (the family-root twin of #1034) #1216's Direction says.tests/test_chain_link_fold.py's new rows usetest_stdout_encoding.run_tool, which reads the bytes and decodes them strictly in the encoding the child was given.run_script, which scripts/validate-repository-naming.py: the plain listing pads each name from the command line or stdin by code points, not by the columns a terminal gives it, so a wide or combining name's label column stands out of line (twin of #1034) #1217 names, decodes in the parent's locale.Left out of scope
adopt-project.pybelongs to 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'slegcolumn lines up underlegwith every other row's (#1034) #1178.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'slegcolumn lines up underlegwith every other row's (#1034) #1178 states them, hold for both copies:wcwidthcounts two count one, as on main: U+4DC0 to U+4DFF and U+3248 to U+324F._print_classificationprints a name from the command line or stdin as it is, notspelledor_shown. An ESC in a name reaches the terminal as itself, and_columnscounts it as one column. That is not scripts/validate-repository-naming.py: the plain listing pads each name from the command line or stdin by code points, not by the columns a terminal gives it, so a wide or combining name's label column stands out of line (twin of #1034) #1217's, and lane 3 may file it.TypeError. Both happen at main as on this branch, and neither is scripts/validate-repository-naming.py: the plain listing pads each name from the command line or stdin by code points, not by the columns a terminal gives it, so a wide or combining name's label column stands out of line (twin of #1034) #1217's:found[0] + role_suffix);--explainof a name no family matches (", ".joinof the ids:sequence item 5: expected str instance, int found).Measured
Scratch probes (
report.py,measure_explain.py) ran each tool at main50ce8ab1, from main's copy of the script, and on this branch, on a UTF-8 stdout and on an ASCII one (PYTHONIOENCODING=ascii:strict, which each script makesbackslashreplace).ascii(name)=column: the name as the row printed it, then the display column its next field begins at.unicodedataby the issue's rule, independently of the code under test.stateis at column 31. Every ASCII label begins at 35.siblings.pyis a real one fromfamily.py initinto local bare remotes. Its members are U+65E5 U+672C U+8A9E,eU+0301coleandAtlas, each mounted atmembers/<name>, and the run is--dry-run.On this branch's ASCII stdout,
members/followed by an escaped name is 26 and 19 columns wide, more than the 18 of the field. Each of those two rows keeps the one space between fields, as:<18keeps it after an ASCII name that long, and nothing is cut.--explainunder the standard policy with itsworkspacefamily's id renamed.zz-wipmatches that family (_family_row; an ASCII id's pattern starts at 29).z zmatches none (_no_family_lines; 23).identicalcompares the whole stdout of main and the branch, andNonemeans main itself prints no such row (", ".joinof a number raises before it).Tests
Every run used a stub
ghfirst on PATH (exit 97),GH_TOKEN=bogus, an emptyGH_CONFIG_DIR,GIT_SSH_COMMAND=falseand a scratchGIT_CONFIG_GLOBAL, one pytest at a time. Each was/usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch> -o tmp_path_retention_policy=failed <file>, at352dcbc966d1c42d8510b2d61fedcda533c725cd.New rows:
tests/test_siblings_family_name.py:test_the_layout_keeps_every_state_under_its_headingandtest_what_ran_keeps_every_state_under_its_heading, each on a UTF-8 and an ASCII stdout.tests/test_chain_link_fold.py:test_a_wide_or_combining_name_has_its_label_under_codexglass_s, on both stdouts.tests/test_pinned_copy_columns.py(new):_padded, and_printedunder seven stdouts, for each copy;--explainid rows on both stdouts;Every new row that reads a table fails at main (main's script swapped in, then restored and checked with
cmp):siblings.pyrows, at('<wide>', 34) != ('<wide>', 31);--explainid rows.The two rows for a number or boolean id pass at main too, because they pin main's bytes.
Gates: my files' tests, every test file that names
siblings.pyorvalidate-repository-naming.py, and the docs and hygiene guards.combinedis one run oftests/test_pinned_copy_columns.py,tests/test_pinned_copy_text.py,tests/test_siblings_family_name.pyandtests/test_chain_link_fold.py. The last 15 files in the list also ran with-rs.tests/test_adopt_e2e.pyandtests/test_adopt_nested_attributes.pyfirst ran beforegit-filter-repowas on the stub PATH, and skipped 27 and 7 rows. Their lines below are the reruns with it on the PATH, with no row skipped.tests/test_family_member_make_jobserver.py: 4 failed,test_under_a_named_pipe_a_member_s_make_before_4_4_runs_serially[bootstrap|validate|park|resume], each atassert "jobserver" not in said. It fails identically at main on this bench (make jobserver warning under the stub PATH); passes in CI. At main50ce8ab1, under the same environment,-k named_pipegives the same 4 failed, with 42 passed, 1 skipped and 68 deselected.Mutants
Each mutant was applied to the committed file, its rows were run, and the file was restored and checked with
cmpagainst HEAD. All 16 were KILLED.Sibling.rowpads the name with:<18again (main's line)test_siblings_family_name.py -k keeps_every_stateSibling.rowpads without_printed_paddedcountslen(the issue's mutant)_char_columnsgives Mn, Me, Cf and the jamo 1 columntest_pinned_copy_columns.pyMEDIAL_AND_FINAL_JAMO_EXTENDED_BemptyTWO_COLUMN_WIDTHSwithoutF_printedwritesbackslashreplacewhatever stdout's handler:<32again (main's line)test_chain_link_fold.py -k label_under_codexglass_printed_paddedcountslen(the issue's mutant)_char_columnsgives W and F 1 columntest_pinned_copy_columns.py_family_id_paddedpads every id withformat_family_id_paddedpads every id by columns (5andtruetoo)_family_id_paddedpads without_printed_no_family_linespads the id with:<18again_family_rowpads the id with:<18againMerges
git merge-tree --write-treeof this head with each, re-listed live withgh pr list --state open --json number,filesfiltered to this PR's files:0ed3e57f(two commits past this branch's base50ce8ab1, touching none of this PR's files): clean, tree0fd5a789..gitthis user cannot read is refusedgit-repo-unreadable, exit 2, not a traceback ornot-a-git-repo, and the doctor'smembersrow names it once, never drift (#1148) #11725aa0f116(templates/family-root/scripts/siblings.py): clean, treee30259cd..gitmodulesthis user cannot read, as through a link into a folder it cannot search, isgitmodules-unreadable, never no section forremove's rollback or no url formake siblings(#1110) #116474096eb4(templates/family-root/scripts/siblings.py): clean, tree9531edfd.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'slegcolumn lines up underlegwith every other row's (#1034) #1178c4beba19(adopt-project.py, whoseplantable this rule copies; none of this PR's files): clean, treead19ba53.Re-listed at 2026-10-23T21:40Z: 48 open PRs, of which #1172 and #1164 touch this PR's files.
Closes #1216
Closes #1217
Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code