Skip to content

standard.py: the package's openRepoShape refuses a repository or ref holding U+202E, U+200B or another character the launcher refuses, by code point, before --version or any git call names it, and refuses a.lock/b, a/.b, a//b, a/b/ and a. before it clones, in the launcher's words (#939, #941) - #947

Merged
brettheap merged 2 commits into
mainfrom
l3-939-941-package-standard
Oct 18, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #939.
Closes #941.

Lane 2 filed both issues and suggested pairing them, because they share standard.py's ref_fault. One commit covers both, since each edits the same docstring and the same comment. cli.py is unchanged: the checks stand in standard.clone_url and standard.target, which --version (cli.print_version) and prepare already call before any line or git call names a value.

Where the rules come from (the launcher, openRepoShape at 64464d31)

#939: what changed, by section

  • --version printed a repository holding U+202E or U+200B as typed, exit 0. standard.py now carries the launcher's list as MISDRAWN_CHARACTERS, entry for entry, and misdrawn_characters, which names what a value holds. clone_url asks it first, on the repository as typed, and refuses in checked_repo's words. Then comes its old check, a leading - or a control character. --version calls clone_url before it prints, so it now exits 2 with the launcher's sentence and no byte of the value.
  • A ref holding either was refused, but quoted as typed. ref_fault asks misdrawn_characters between the length and the control check, where the launcher asks it, in ref_character_fault's words. The shape refusal, which quotes the ref, is reached only by a value that passed all four checks.
  • A run that reached the clone named the repository and handed git the URL. prepare calls target and clone_url before it looks for git, so --doctor, a scaffold run, --shape-ref and the four tools refuse before any git call. Measured by hand on a stamped copy with a logging git first on PATH, for --doctor ., Atlas --org acme and shape-doctor --json: a repository with U+202E, a ref with U+202E and the ref a.lock/b each made 0 git calls, with no E2 80 AE on either stream. A clean acme/r made 1 call (git clone).
  • The table is a copy, held equal by a test. The package ships alone and reads no file of the launcher's. The entries are the launcher's hex strings, as written, so the parity test compares text with text, as install.sh's does. That also keeps the package pure ASCII, which test_the_package_is_pure_ascii requires.
  • The words. ref_fault's docstring and the comment above CONTROL_RE now state the five checks in order and name the tests that hold the copy to the launcher.
  • Decided, as the Direction asks: runs that never read the values are left as they are. A run beside a git checkout and --preflight do not refuse either value. Neither run prints a value or hands one to git (prepare and print_version return first beside a git checkout, and --preflight returns before prepare), so the narrower direction refuses nothing that could reach a line there. A third run reads only half: a scaffold run with a clean --shape-ref never asks $OPENREPOSHAPE_REF anything (target returns the typed ref first), so that value is never printed or sent, though the launcher refuses it. A check in main_openreposhape would also have to be repeated in run_tool for the four tools.

#941: what changed, by section

  • The shape line. It now holds setup.sh, install.sh and the openRepoShape command: each keeps its own copy of the old ref rule (*..* | *.lock), so a ref git refuses part by part (a.lock/b, a/.b, a//b) clones the standard before it fails #874's rule: REF_RE, no .., no empty part (//, or a / at the end), no part that starts with . or ends in .lock, and no . at the end. That is setup-project.py's why_not_a_ref rule. I measured the issue's 3,110 names (every name of up to five pieces from a, 0, ., /, -, .lock that REF_RE takes). This rule disagrees with git check-ref-format --branch on 0 of them, and with the launcher's own ref_fault, lifted out and run by bash, on 0. origin/main's line disagrees with each on 1,122.
  • A ref git refuses reached git. a.lock/b, a/.b, a//b, a/b/ and a. are now refused by target, before remote_commit and the clone: 0 git calls (see the tests below).
  • The sentence is the launcher's, word for word (REF_RULE_WORDS). --shape-ref gets the same sentence with its own opening.
  • The docstring and the comment state the order and the rule, and name the test.
  • No test held the rule to git's. test_ref_fault_takes_a_ref_as_git_does_and_refuses_the_rest is now the fourth reader of REF_PARTS. It runs in-process with assert_a_copy_answers_as_git_does("standard.py", ...) and assert_the_refusal_states_the_ref_rule, and git is asked about each name, never a repository.

Tests added (tests/test_openreposhape_package.py), 115 cases

Left out of scope

Test runs

Each run was a separate pytest process, one at a time, with a stub gh (exit 97) first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false:
timeout 2400 /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed <file>

  • tests/test_openreposhape_package.py at origin/main (64464d31): 58 passed in 20.37s
  • tests/test_openreposhape_package.py at this head (21558e44): 173 passed in 28.66s
  • tests/test_openreposhape_command.py: 447 passed in 46.44s
  • tests/test_agents_md_codes.py: 47 passed in 1.75s
  • tests/test_cli_reference.py: 23 passed in 136.99s (0:02:16)
  • tests/test_update_add_codes_documented.py: 6 passed in 0.48s
  • tests/test_repo_hygiene.py: 81 passed in 3.41s
  • tests/test_no_auto_maintenance.py: 60 passed in 12.58s
  • tests/test_validator_child_encoding.py (it names cli.py): 163 passed in 71.79s (0:01:11)
  • tests/test_release_workflow.py (it names src/openreposhape/): 56 passed in 3.88s
  • tests/test_setup_sh.py (a REF_PARTS reader, unchanged): 125 passed in 88.01s (0:01:28)
  • tests/test_install_sh.py (a REF_PARTS reader, unchanged): 270 passed in 40.76s
  • tests/test_shape_doctor.py: not run, because neither shape-doctor.py nor a helper it imports changed.

No new refusal code was added (the package's refusals carry none), so no code doc changes. No doc quoted the old ref sentence: at origin/main, git grep finds it only in standard.py.

Mutants

Each mutant was applied to standard.py and run against the whole of tests/test_openreposhape_package.py. Each is killed.

# mutant failed killed by (among others)
m00 origin/main's standard.py 65 the U+202E and U+200B rows and 4 order rows under both locales (16), the 21 no-git-call runs, the 2 --shape-ref cases, the 2 table tests, 19 REF_PARTS rows and the 5 added refs. The control rows, the length row, NOT_CONTROLS, the help case and the 17 rows git takes pass there as well, as they should
m01 clone_url without the drawn-as-another check 11 test_version_refuses_...[repo-u+202e-C], test_a_run_that_clones_...[doctor-repo-u+202e]
m02 ref_fault without it 12 test_version_refuses_...[ref-u+202e-C], test_a_typed_shape_ref_...[u+202e]
m03 ref_fault asks the control check first 2 test_version_refuses_...[ref-an-escape-then-u+202e-*]
m04 clone_url asks the control check first 2 test_version_refuses_...[repo-an-escape-then-u+202e-*]
m05 the table without U+200B 10 test_the_misdrawn_table_is_the_launchers_list, ...[repo-u+200b-C]
m06 the range U+2028 to U+202E cut at U+202D 21 test_misdrawn_characters_names_the_launchers_and_nothing_else
m07 names in the value's order, not code point order 5 test_version_refuses_...[repo-two-at-once-*], [ref-two-at-once-*]
m08 a range's last code point left out 25 the table tests, every U+202E case
m09 code points named in lower case 25 the table tests, every U+202E case
m10 drawn-as-another asked before the length 2 test_version_refuses_...[ref-over-long-and-u+202e-*]
m11 the old shape line back, with the new sentence 34 test_ref_fault_takes_...[a.lock/b] and 12 other rows, the 5 refs in each run
m12 no empty-part check 10 test_ref_fault_takes_...[a//b], [a/b/]
m13 no check for a part that starts with . 8 test_ref_fault_takes_...[a/.b], [a/b/.c/d]
m14 .lock asked of the end of the name only 8 test_ref_fault_takes_...[a.lock/b], [release.lock/v1]
m15 no check for a name that ends in . 6 test_ref_fault_takes_...[a.], [a/b.]
m16 a part that ends in . refused (over-refusal) 1 test_ref_fault_takes_...[a./b]
m17 the first character lower case only (over-refusal) 1 test_ref_fault_takes_...[Feature/ABC-123]
m18 a part that starts with _ or - refused (over-refusal) 2 test_ref_fault_takes_...[fix/_tmp], [fix/-wip]
m19 the sentence without its empty-part clause 35 every refused REF_PARTS row, every shape refusal in a run

Merges

  • git merge-tree --write-tree of this head with origin/main (64464d31): clean, tree f4a9282699321be04716eaf639a234dd4cd8261d, exit 0.
  • At 2026-10-18T16:47Z, none of the 10 open PRs touched src/openreposhape/standard.py, src/openreposhape/cli.py or tests/test_openreposhape_package.py, so there was nothing else to merge with.

Fix round 1 (head c1de4c86, after rv947's review of 21558e44)

rv947 found 21558e44 READY: one SHOULD, which is this text, and NITs. The sections above describe 21558e44; this round changes the head as follows. One commit, 3 files, 38 insertions and 10 deletions.

  • SHOULD-1 (text only). The "Decided" item claims "beside a git checkout" and no more. Beside a source tree with no .git (a ZIP, or an sdist installed with pip install -e .), that tree's setup-project.py self-bootstraps and hands $OPENREPOSHAPE_REPO to git clone without the drawn-as-another check. That is the same at origin/main, and it is now setup-project.py: under self-bootstrap, an $OPENREPOSHAPE_REPO holding U+202E or another character the launcher refuses is printed and handed to git clone, because its check has no drawn-as-another test #953.
  • NIT-2 (text only). The "Decided" item names the third run that never reads a value: a scaffold run with a clean --shape-ref never asks $OPENREPOSHAPE_REF.
  • NIT-1. In standard.py's prepare, repo = repository() and url = clone_url(repo) now come before wanted = target(stamp, typed_ref), with a comment and a docstring line that say the order. Nothing between them depended on the old order: clone_url reads only $OPENREPOSHAPE_REPO, and target reads the stamp, the typed --shape-ref and $OPENREPOSHAPE_REF. With both values bad, --doctor, a scaffold run and the tools now name the repository's fault first, as the launcher and the package's --version do.
    • Test: test_with_both_values_bad_a_run_names_the_repository_first in tests/test_openreposhape_package.py, mirroring the launcher's test_bad_values_are_refused_in_the_order_the_check_states. It runs --doctor ., Atlas --org acme and shape-doctor --json (3 cases) with a repository holding U+202E and a ref holding an escape, and wants exit 2, REFUSED: OPENREPOSHAPE_REPO holds ..., no mention of OPENREPOSHAPE_REF, no git call and neither value echoed.
    • Mutant: the old order back in prepare. It fails the 3 cases ([doctor], [scaffold] and [shape-doctor]) and nothing else: 3 failed, 174 passed.
  • NIT-3. ("a/./b", False) is in REF_PARTS in tests/conftest.py, and the comment above the table counts the names the old line took as fourteen. git check-ref-format --branch a/./b answers fatal: 'a/./b' is not a valid branch name. The four files that read REF_PARTS (package, launcher, test_setup_sh.py, test_install_sh.py) each gain one case, and all pass.
    • Mutant: a part that is exactly . is taken (rv947's own2). Before this round it survived, with 173 passed. With the new row deselected it still survives here (176 passed, 1 deselected, the 176 including the 3 new cases above). With the row, it fails test_ref_fault_takes_a_ref_as_git_does_and_refuses_the_rest[a/./b] and nothing else: 1 failed, 176 passed.
  • NIT-4. test_version_prints_a_repository_holding_none_of_the_list carries NO_NON_ASCII_INTO_GIT_BASH, imported from the launcher's tests, as the launcher's twin test_version_prints_a_path_a_utf_8_locale_prints does: it needs a UTF-8 stdout in the child, so it is skipped on Windows.
  • NIT-5 needs no change here: whoever lands second reconciles, if openRepoShape, setup.sh and install.sh: under the C locale a $OPENREPOSHAPE_REPO URL or path, or a ref, with a letter past ASCII is refused as "a control character", so --version, --install and --preflight refuse what v1.1.0 ran #940 adds rows to NOT_CONTROLS or another imported table.

Test runs at c1de4c86, each a separate pytest process under the stub gh, GH_TOKEN=bogus, an empty GH_CONFIG_DIR and GIT_SSH_COMMAND=false, wrapped in timeout 2400:

  • tests/test_openreposhape_package.py: 177 passed in 23.31s (173, the order test's 3 and the new row)
  • tests/test_openreposhape_command.py: 448 passed in 36.76s
  • tests/test_setup_sh.py: 126 passed in 69.01s (0:01:09)
  • tests/test_install_sh.py: 271 passed in 27.20s
  • tests/test_repo_hygiene.py: 81 passed in 1.84s
  • tests/test_no_auto_maintenance.py: 60 passed in 5.79s

Merges: git merge-tree --write-tree of c1de4c86 with origin/main (64464d31) is clean, tree 0c09d9510683a514d5fa6ef2856b82928de35a4e. With #949's head (8c5d6051), which also edits tests/conftest.py, it is clean too, tree 1951cc0bbca9ed5634ee8c0a2d2b521310b42660.

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

…f holding U+202E, U+200B or another character the launcher refuses, by code point, before `--version` or any `git` call names it, and refuses `a.lock/b`, `a/.b`, `a//b`, `a/b/` and `a.` before it clones, in the launcher's words (#939, #941)

#939: `clone_url` and `ref_fault` held a value to `CONTROL_RE` only, which
holds nothing above U+009F. So `--version` printed a repository holding
U+202E or U+200B as typed and exited 0, a ref holding either was refused
with the value quoted, and a run that reached the clone handed `git` the
URL with it. `standard.py` now carries the launcher's
`MISDRAWN_CHARACTERS` as a table, entry for entry, and
`misdrawn_characters`, which names what a value holds as `U+XXXX` in code
point order. `clone_url` asks it first, on the repository as typed, and
`ref_fault` asks it between the length and the control character: the
launcher's order (`checked_repo`, `ref_character_fault`) and its two
sentences. Both checks already stand before every line and every `git`
call that names a value, so no run changes where it refuses. A run beside
a checkout and `--preflight` still never read the values.

#941: `ref_fault` kept the old shape line, which asked `..` and `.lock` of
the whole name only, so a ref git refuses part by part cost a
`git ls-remote`, a full clone and a failed `git checkout`. It now holds
#874's rule (no `..`, no empty part, no part that starts with `.` or ends
in `.lock`, no `.` at the end) and the launcher's sentence, word for word.
Over the 3,110 names the issue measured, it disagrees with git on 0
(1,122 before).

The docstring and the comment above `CONTROL_RE` state the five checks in
their order and name the tests that hold the copy to the launcher.

tests/test_openreposhape_package.py: the launcher's `UNPRINTABLE_VALUES`
through `--version` under `C` and `C.UTF-8`, with cases that hold the order;
the launcher's `NOT_CONTROLS` still printed; `--doctor`, a scaffold run,
`shape-doctor` and `--shape-ref` refusing before any `git` call; the table
held to the launcher's list and `SHARING_ITS_BYTES`; `REF_PARTS` against
`ref_fault` and git; #874's five refs in
`test_a_ref_is_checked_before_anything_is_cloned`; and the launcher's
`test_help_answers_past_a_repository_and_ref_no_line_can_print`, imported.

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

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR hardens repository and ref validation to match the launcher and git: visually ambiguous Unicode is rejected by code point before values can be printed or passed to git, and refs are validated component-by-component before cloning. Extensive parity and integration tests verify exact ordering, wording, locale behavior, no-leak guarantees, and zero external calls on refusal.

Sequence diagram for preflight repository and ref validation

sequenceDiagram
    participant CLI
    participant Prepare
    participant Standard
    participant Git

    CLI->>Prepare: prepare()
    Prepare->>Standard: target(ref)
    Standard->>Standard: ref_fault(ref, what)
    alt ref_fault returns refusal
        Standard-->>Prepare: refusal
        Prepare-->>CLI: exit 2 without value or git call
    else ref passes
        Prepare->>Standard: clone_url(repo)
        Standard->>Standard: misdrawn_characters(repo)
        alt repository is unsafe
            Standard-->>Prepare: refusal
            Prepare-->>CLI: exit 2 without value or git call
        else repository passes
            Prepare->>Git: clone URL
            Git-->>Prepare: result
        end
    end
Loading

Flow diagram for ordered repository and ref checks

flowchart TD
    Start([Input repository or ref]) --> Kind{Value type}
    Kind -->|Repository| RepoUnicode[Check misdrawn_characters]
    RepoUnicode -->|Found| RepoReject([Refuse without echo or git])
    RepoUnicode -->|None| RepoLegacy[Check leading dash or control character]
    RepoLegacy -->|Invalid| RepoReject
    RepoLegacy -->|Valid| RepoAccepted([Repository accepted])
    Kind -->|Ref| RefEmpty[Check empty]
    RefEmpty -->|Invalid| RefReject([Refuse])
    RefEmpty -->|Valid| RefLength[Check length]
    RefLength -->|Over limit| RefReject
    RefLength -->|Valid| RefUnicode[Check misdrawn_characters]
    RefUnicode -->|Found| RefReject
    RefUnicode -->|None| RefControl[Check control character]
    RefControl -->|Found| RefReject
    RefControl -->|None| RefShape[Check REF_RE and component rules]
    RefShape -->|Invalid| RefReject
    RefShape -->|Valid| RefAccepted([Ref accepted])
Loading

File-Level Changes

Change Details Files
Rejects launcher-defined visually ambiguous characters in repositories and refs before any output or external command can expose them.
  • Adds an ASCII copy of the launcher's misdrawn-character code-point table and a helper that reports matching code points in canonical order.
  • Checks repositories in clone_url and refs in ref_fault, preserving launcher validation order and refusal wording.
  • Adds coverage for locale behavior, mixed invalid characters, length precedence, and no-leak/no-git-call guarantees.
src/openreposhape/standard.py
tests/test_openreposhape_package.py
Aligns ref validation with git and the launcher's per-path-component shape rules.
  • Rejects empty path components, dot-prefixed components, components ending in .lock, and refs ending in a dot.
  • Keeps validation before cloning and uses the launcher’s ref-rule sentence for failures.
  • Cross-checks the package against git behavior and launcher fixtures, including previously accepted invalid refs.
src/openreposhape/standard.py
tests/test_openreposhape_package.py
Expands package-level integration and parity tests to verify validation boundaries across commands.
  • Tests --version, --shape-ref, doctor, scaffolding, and shape-doctor with logging command stubs.
  • Factors reusable command loggers and imports launcher fixtures for exact parity checks.
  • Adds table-parity, refusal-wording, no-value-echo, and pre-git-call assertions.
tests/test_openreposhape_package.py

Assessment against linked issues

Issue Objective Addressed Explanation
#939 Apply the launcher's misdrawn-character check to repository values, environment refs, and --shape-ref, using the same character list, refusal wording, and check order. ✅
#939 Refuse affected values before --version prints them or a package run prints, clones, or passes them to git, without echoing the values; decide whether runs that never read the values should also refuse them. ✅
#941 Update the package’s ref_fault to apply the launcher’s part-by-part Git ref rules and refuse invalid refs before any Git call, using the launcher’s refusal wording. ✅
#941 Add package tests that compare its ref acceptance and refusal wording with Git and the launcher, including refs the old rule accepted but Git rejects. ✅
#941 Document the corrected rule and its ordering in the package’s ref_fault docstring and related comment. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T17:08:26Z) RV947's independent review of #947 at 21558e44 (origin/main 64464d31): READY. No MUST, one SHOULD (the body's text, plus a follow-up issue), five NITs. RV947 is lane 3's opus reviewer, not the writer.

  1. Parity, measured by RV947's own scripts, which read both files:

    • MISDRAWN_CHARACTERS equals openRepoShape:501 entry for entry, in order. Both expand to the same 41 code points.
    • misdrawn_characters, run over all of Unicode but the surrogates, names exactly those 41.
    • Run by bash under LC_ALL=C, the launcher's function and the package's agree on all 81 edge values: first-2 to last+2 of every range, all at once, and two at once. Names come in code point order.
    • The --version refusals are byte-identical to the launcher's in 11 cases, under C and C.UTF-8.
  2. The ref rule (openreposhape package: ref_fault keeps the old ref rule, so it takes a.lock/b, a/.b, a//b, a/b/ and a., clones the standard in full and fails at git checkout, where the launcher, setup.sh and install.sh refuse them first (#874) #941):

    • Over the issue's 3,110 names, the head disagrees with git (--branch) on 0 and with the launcher's ref_fault, run by bash, on 0. The refusal text is identical. origin/main disagreed on 1,122.
    • Over 5,450 more names, the only disagreement with git is HEAD, which every copy takes by design.
    • Hard cases:
      • @{, a@{b, ., .., a/, a/b/, -x, refs/heads/.., a/.lock and the rest are refused by all three.
      • refs/heads/x and a 255-character name are taken by all; 256 is refused for length.
      • @, a@b and non-ASCII letters are taken by git and refused by both copies, in the same words.
  3. Paths:

    • --version, --doctor, a scaffold run and shape-doctor refuse before any git call. Measured: 0 calls, and no E2 80 AE bytes.
    • --preflight prints neither value.
    • Beside a git checkout, neither value is printed.
  4. Tests, one process at a time under a stub gh:

    File Result
    package 173 passed
    command 447 passed
    test_repo_hygiene.py 81 passed
    test_agents_md_codes.py 47 passed
    test_no_auto_maintenance.py 60 passed

    The new tests read MISDRAWN_CHARACTERS out of openRepoShape and import the launcher's tables and words, so parity is enforced and not copied.

  5. Mutants (failed tests):

    Mutant Failed
    m01 11
    m08 21
    m11 34
    m14 8
    U+202C dropped from the table 2
    U+202C skipped by the function alone 2
    no .. check (a..b only) 1

    One survives: a part that is exactly . is taken (NIT-3).

  6. Merges: clean with origin/main (tree f4a92826, the same as the writer's) and with setup-project.py and validate-repository-naming.py: step (4) reads and names what step (5) does, --link-remedy reads a link holding =, strips its words and notes an unused one, and the Windows hand clone passes -c core.autocrlf=false (#907, #926) #949 (tree 9399716b). No other open PR touches these files.

  7. Commit and body: in the house form, "Closes openreposhape package: openRepoShape --version prints a repository holding U+202E or U+200B as typed and exits 0, and a ref holding either is refused with the value as typed, where the launcher refuses both before it prints (#866) #939." and "Closes openreposhape package: ref_fault keeps the old ref rule, so it takes a.lock/b, a/.b, a//b, a/b/ and a., clones the standard in full and fails at git checkout, where the launcher, setup.sh and install.sh refuse them first (#874) #941.", with the right author and opus trailer.

SHOULD-1. The body says the runs left alone never print a value or hand one to git. That is false beside a source tree with no .git, such as a ZIP, or an sdist installed with pip install -e ..

  • beside_checkout checks for files only, so that tree's setup-project.py self-bootstraps.
  • Measured: stdout holds git clone --quiet -- acme/r[U+202E]forged ..., and git is called with it. setup-project.py:881-882's checked_value has no drawn-as-another check.
  • It is the same at origin/main, so this PR does not regress it.
  • Fix: narrow the claim to "beside a git checkout". Lane 3 files the follow-up for setup-project.py's self-bootstrap check.

NITs:

Next: lane 3 runs a short fix round for SHOULD-1's text and NITs 1-4, and RV947 re-checks the delta. Then #947 lands under Brett Heap's v1.2.0 merge word, once tests is green.

Lane: openreposhape-3 (openRepoShape-3)

… the repository's fault first, as the launcher does, and the ref table holds a lone `.` part (#939, #941)

Fix round 1 of #947, after rv947's review (READY; one SHOULD, which is the
pull request's text, and five NITs).

rv947's NIT-1: `prepare` asked `target(stamp, typed_ref)` before
`repository()` and `clone_url(repo)`, so with `$OPENREPOSHAPE_REPO` and
`$OPENREPOSHAPE_REF` both bad, `--doctor`, a scaffold run and the four tools
named the ref's fault. The launcher and the package's own `--version` name
the repository's first, and #939's Direction says "the repository first".
The two lines move above `target`; nothing between them reads the other
(`target` reads the stamp, `--shape-ref` and `$OPENREPOSHAPE_REF`; the
repository check reads `$OPENREPOSHAPE_REPO`), and the docstring of
`prepare` says the order.

rv947's NIT-3: `REF_PARTS` gains `("a/./b", False)`. Git, the launcher and
the package refuse it, and a copy that took a part that is exactly `.`
survived the table, because `a/.` is refused for ending in `.` as well. The
comment above the table counts the names the old line took as fourteen.

rv947's NIT-4: `test_version_prints_a_repository_holding_none_of_the_list`
needs a UTF-8 stdout in the child, so it carries the launcher's
`NO_NON_ASCII_INTO_GIT_BASH` skip on Windows, as the launcher's twin does.

tests/test_openreposhape_package.py:
`test_with_both_values_bad_a_run_names_the_repository_first`, for `--doctor`,
a scaffold run and `shape-doctor`: a repository holding U+202E and a ref
holding an escape give the repository's refusal and only it, no `git` call,
neither value echoed (the launcher's
`test_bad_values_are_refused_in_the_order_the_check_states`). The old order
fails its three cases. A part that is exactly `.` taken fails
`test_ref_fault_takes_a_ref_as_git_does_and_refuses_the_rest[a/./b]`, and
the launcher's, `setup.sh`'s and `install.sh`'s tests read the same row.

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

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-18T17:24:15Z) RV947 round 2, on the delta 21558e44..c1de4c86 (fix round 1): READY. No new MUST or SHOULD, and one new cosmetic NIT.

  1. The delta, read in full: three files (standard.py, tests/conftest.py, tests/test_openreposhape_package.py) and nothing else.

    • prepare now asks repository() and clone_url() before target().
    • "first fourteen" is right: the old line takes the first 14 rows and refuses topic.lock.
    • The NO_NON_ASCII_INTO_GIT_BASH mark matches the launcher's twin.
    • The Sonnet trailer is correct.
  2. The order: with the repository holding U+202E and the ref holding ESC, the package's --doctor ., Atlas --org acme and shape-doctor --json now give the launcher's refusal byte for byte (REFUSED: OPENREPOSHAPE_REPO holds ... (U+202E) ...). That holds under C and C.UTF-8, with exit 2, 0 calls, and no bytes of either value.

  3. a/./b: all four REF_PARTS readers (setup.sh's, install.sh's, the launcher's and the package's) run the row and refuse it, as git does. No reader has a skip.

  4. Tests, one process at a time under a stub gh:

    File Result
    package 177 passed
    command 448 passed
    test_setup_sh.py 126 passed
    test_install_sh.py 271 passed
    test_agents_md_codes.py 47 passed
    test_repo_hygiene.py 81 passed
  5. Mutants:

    • The old order fails 3: test_with_both_values_bad_a_run_names_the_repository_first[doctor], [scaffold] and [shape-doctor].
    • own2 fails 1: test_ref_fault_takes_a_ref_as_git_does_and_refuses_the_rest[a/./b]. It survived in round 1.
  6. The PR body: the SHOULD-1 and NIT-2 text is true; setup-project.py: under self-bootstrap, an $OPENREPOSHAPE_REPO holding U+202E or another character the launcher refuses is printed and handed to git clone, because its check has no drawn-as-another test #953 exists and matches; the last two lines are the Lane line and the robot line.

  7. Merges: clean with origin/main 64464d31 (tree 0c09d951) and with setup-project.py and validate-repository-naming.py: step (4) reads and names what step (5) does, --link-remedy reads a link holding =, strips its words and notes an unused one, and the Windows hand clone passes -c core.autocrlf=false (#907, #926) #949 8c5d6051 (tree 1951cc0b), the same trees the body names.

NIT-6 (new, cosmetic): one line of prepare's docstring (standard.py:698) is 104 characters, where the rest of the paragraph wraps at about 76. No test enforces line length. Lane 3 leaves it for the next PR that touches standard.py, rather than restart tests here.

#947 lands under Brett Heap's v1.2.0 merge word once tests is green on c1de4c86, in its turn on #473.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 5f62ea4 into main Oct 18, 2026
5 checks passed
@brettheap
brettheap deleted the l3-939-941-package-standard branch October 18, 2026 18:53
brettheap added a commit that referenced this pull request Oct 20, 2026
… holding U+202E or another character the launcher refuses, by code point and in the launcher's words, before it prints the value or runs `git clone` (#953) (#1043)

`setup-project.py`'s self-bootstrap refuses an `$OPENREPOSHAPE_REPO` holding a character the launcher refuses (U+202E and the rest of `MISDRAWN_CHARACTERS`). It refuses by code point, in the launcher's words, before it prints the value or hands it to git.

Head `c42afa30`, one commit on origin/main `5b41d44f`. Only `setup-project.py` and a new `tests/test_setup_project_repo_misdrawn.py` change. No flag, no `--help` line and no doc changes.

**`setup-project.py`** (section 0, just above `self_bootstrap`):
- `MISDRAWN_CHARACTERS` is the launcher's list (`openRepoShape:502` at `5b41d44f`), entry for entry, in the package's tuple form. It is a copy, for `checked_value`'s reason: self-bootstrap mode runs before there is a checkout to read the launcher from.
- `misdrawn_characters(value)` names the characters a value holds as `U+XXXX`, in code point order. It is the package's `standard.misdrawn_characters`, and it looks for characters, as `CONTROL_RE` does.
- `checked_repo(value)` asks that first. It refuses with the launcher's `checked_repo` sentence, which names the code points and nothing else of the value. Then it calls `checked_value(..., PATH_RE)`, unchanged, for a leading `-` and a control character.
- `self_bootstrap` reads a non-empty `$OPENREPOSHAPE_REPO` through `checked_repo`. Unset or empty still means the default. The check stays where `--org` is asked, so the order is the same: `--org` first, then the repository, then `openRepoShape setup` is printed, then `mkdtemp` and `git clone`.
- The value's raw bytes are never printed. The refusal goes through `die`, so it is `REFUSED: ...` on stderr with exit 2.

**`tests/test_setup_project_repo_misdrawn.py`**, 22 tests:
- **Parity:** `test_the_misdrawn_table_is_the_launchers_list` reads `MISDRAWN_CHARACTERS=(...)` out of `openRepoShape` and compares the tuple. It also checks that `misdrawn_characters` names all of the launcher tests' `MISDRAWN`, each once and in order. `test_misdrawn_characters_names_the_launchers_and_nothing_else` runs the launcher's `SHARING_ITS_BYTES`. These follow #947's two tests for the package.
- **Direct path, 12 tests:** two runs (`Atlas --org acme`, which is the issue's run, and `--org acme` alone) times six values: U+202E, U+00AD, U+200B, U+E0001 (two, three and four UTF-8 bytes), two characters at once, and a tab before U+202E (the launcher asks for the code point before the control character).
  - Each test runs a lone copy of the file under `call_loggers`' logging git, gh and curl.
  - Each checks the launcher tests' `assert_refused_saying`: exit 2, the launcher's sentence, an empty stdout, and no listed character, escape or `forged` in either stream. So no `E2 80 AE` gets out.
  - The git log must hold exactly one call: the checkout probe.
- **Values that still go on, 7 tests:** the launcher's `NOT_CONTROLS` after `acme/`: e-acute, a no-break space, U+200C and U+200D where a script or an emoji needs them, U+2026, U+FF21 and a subdivision flag. Each is printed in the clone line and handed to `git clone` as before.
- **Package path, 1 test:** a tree with no `.git` (the package's modules under `src/openreposhape/`, plus `setup-project.py`, `scaffold-project.py`, `shape-doctor.py` and `contracts/repository-naming.yaml`). `openRepoShape Atlas --org acme` is run from that tree's `src` with the issue's value. The package prints its one line (`the standard is the checkout this package sits in, <tree>.`) and hands over. The tree's `setup-project.py` refuses: exit 2, the launcher's sentence, no U+202E and no `forged` in either stream, and only the checkout probe in the git log.
- Tests that use the logging stub are skipped on Windows. There `subprocess` starts `git.exe` and never the stub's `.cmd`, so the stub would log nothing. CI is Linux only (#597).
- `setup.sh` has no twin of these tests. Its guard covers control characters and the two separators and no other listed character, by #940's design: the launcher refuses the rest first where it is the way in.

**"No git call", stated exactly.** The refused run makes one git call, `git -C <dir> rev-parse --show-toplevel`. That is `is_shape_checkout`, which runs before `self_bootstrap` on every run past `--help` and `--preflight`. It asks whether the file stands in a checkout, which is how the run knows it is self-bootstrapping at all, and it names nothing of the value. No git call names the value, and there is no clone. Moving the check above the probe would also refuse the value on the developer path, which never reads it. It would also leave `setup-project.py` ordered differently from `setup.sh`, which probes first and guards only on its self-bootstrap path.

These run `$Q/v118/w953/probe.py`. Each case uses a fresh scratch folder, a logging git stub that then runs the real git, a gh that fails, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false` and `GIT_CONFIG_GLOBAL=/dev/null`. "Before" is `git archive 5b41d44`; "after" is the worktree. Results are the same under git 2.43.0 and 2.47.1.

| Case | Before (`5b41d44f`) | After |
|---|---|---|
| package, ZIP tree, `Atlas --org acme`, `acme/r[U+202E]forged` | exit 128; `E2 80 AE` on stdout and stderr; git: probe, then `clone --quiet -- acme/r[U+202E]forged /tmp/openreposhape-shape-...` | exit 2; no `E2 80 AE`; git: probe only; stdout is the package's one line |
| direct, `Atlas --org acme`, same value | exit 128; raw bytes in the clone line; probe + clone | exit 2; refusal names `U+202E`; probe only |
| direct, `--org acme`, same value | exit 128; probe + clone | exit 2; probe only |
| direct, `acme/r[U+200B]forged` | exit 128; probe + clone | exit 2; names `U+200B` |
| direct, `acme/r[U+E0001]forged` | exit 128; probe + clone | exit 2; names `U+E0001` |
| direct, `acme/r[U+2026]b` (not listed) | exit 128; probe + clone | the same: probe + clone, exit 128 |

The "after" refusal, from the probe:

```
REFUSED: OPENREPOSHAPE_REPO holds a character a terminal draws as something other than itself (U+202E): one that is invisible, is drawn as another character, ends a line or reorders the text around it. This tool will not put it on a `git clone` command line. Set it to a URL or a path.
```

The new file was run on main's `setup-project.py` (`git show 5b41d44:setup-project.py`, put back byte for byte afterwards): **15 failed, 7 passed**.
- Failed: both parity tests (`AttributeError`: no `MISDRAWN_CHARACTERS`, no `misdrawn_characters`), all 12 direct tests, and the package test.
- Passed: the 7 `NOT_CONTROLS` tests. They pass at main by design, because they guard against a check that is too broad.

Each mutant was applied alone (`$Q/v118/w953/mutate.py`), the new file was run (22 tests), and both files were put back. **All 18 are killed.**

| Mutant | Failed |
|---|---|
| m01 check removed (main's read) | 13 |
| m02a check moved after the `(0) self-bootstrap` line | 13 |
| m02b check moved after the clone line | 13 |
| m03a only the value's first character checked | 15 |
| m03b only the table's first entry checked | 13 |
| m04 only U+202E checked | 8 (U+00AD, U+200B, U+E0001, two-at-once) |
| m05 refusal exits 1 | 13 |
| m06a table drops `E0001` | 4 |
| m06b parity compares the first five entries + table drops `E0001` | 4 (parity's second assert, `SHARING_ITS_BYTES`, both U+E0001 tests) |
| m06c parity compares the first five + table adds `200C` | 2 (`SHARING_ITS_BYTES`, the Persian/U+200C test) |
| m06d parity compares the first five + `2060-206F` narrowed to `206E` | 2 |
| m06e parity uses set inclusion + table drops `E0001` | 4 |
| m07 a range read without its last code point | 15 |
| m08 control check asked before the code points | 2 (both tab-then-U+202E tests) |
| m09 characters named in the value's order | 3 |
| m10 every character past ASCII refused | 7 (every `NOT_CONTROLS` test) |
| m11 refusal quotes the value back | 13 |
| m12 check skipped on a hand-over (`OPENREPOSHAPE_SELF_BOOTSTRAP=1`) | 1 (the package test alone) |

All at `c42afa30`, one pytest process at a time: `/usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed`, one reused basetemp, `EMAIL=service@opensoft.dev`, a stub gh first on PATH, `GH_TOKEN=bogus`, an empty `GH_CONFIG_DIR`, `GIT_SSH_COMMAND=false`, `GIT_CONFIG_GLOBAL=/dev/null`.

| Run | git | Files | Result |
|---|---|---|---|
| h1 | 2.43.0 | `test_setup_project_repo_misdrawn.py` (22), `test_setup_project_ref_parts.py` (27), `test_openreposhape_package.py` (213), `test_readme_windows_quoting_sentence.py` (44), `test_release_install_check_children.py` (30), `test_repo_hygiene.py` (100), `test_agents_md_codes.py` (47), `test_cli_reference.py` (23) | 506 passed in 94.06s |
| h2 | 2.43.0 | `test_setup_project_py.py` | 340 passed in 122.68s |
| h3 | 2.43.0 | `test_setup_sh.py` | 191 passed in 96.62s |
| h4 | 2.47.1 | `test_setup_project_repo_misdrawn.py`, `test_setup_project_ref_parts.py`, `test_setup_project_py.py` | 389 passed in 128.13s |
| h5 | 2.43.0 | `test_no_auto_maintenance.py`, `test_setup_project_dry_run_offers.py`, `test_validator_child_encoding.py`, `test_setup_project_name_edges.py`, `test_setup_project_typed_name.py` | 353 passed, 2 skipped in 73.92s (the two skips: `GIT_CONFIG_GLOBAL` was set) |

No `shell_quoted` call was added (count 0), so the README's and the handbook's Windows-quoting sentences and `NAMED` are unchanged. `setup-project.py` is not a pinned copy. The wheel takes it at build time (`pyproject.toml` `force-include`), and the tests that read its source (`test_setup_project_py.py`, `test_release_install_check_children.py`, `test_repo_hygiene.py`) pass.

- `git merge-tree --write-tree` of `c42afa30` with origin/main `5b41d44f` (fetched with `-c gc.auto=0 -c maintenance.auto=false`) is clean, tree `59de4c261c228274a5cff18aff953211de7002e3`.
- No open PR touches `setup-project.py` or the new test file. Every open PR's head was still merged in, and all ten are clean: #1041 `4a23c6e2`, #1039 `5f924500`, #1038 `fd1de5ab`, #1031 `eea70be1`, #1030 `9b4845bb`, #1028 `0c9eac6c`, #1027 `052f1e24`, #1025 `f83ca618`, #1023 `4d0aa1cf`, #954 `dc7fd7ea`.

- **RV947's companion is left out.** That companion is the package's scaffold run asking the repository even beside a checkout.
  - Once this lands, no package run prints the value or hands it to git. Beside a git checkout, the hand-over takes `setup-project.py`'s developer path, which never reads `$OPENREPOSHAPE_REPO`. `self_bootstrap` and `standard.py` are the only shipped Python that read it. Beside a tree with no `.git`, this check refuses it.
  - The companion would change `standard.prepare`, which `--doctor` and the four tools share. They would then refuse a value they never use. That is the launcher's "checked once, before every run but `--help`" policy, and it is a decision about the package of its own.
  - `src/openreposhape/standard.py` is also open in #1038.
- **Found while measuring, not changed here:** `CONTROL_RE` and `PATH_RE` hold `\x00-\x1f\x7f` only, so a C1 control passes `checked_value`. The launcher and the package refuse C1 (U+0080 to U+009F) in a repository. With `acme/r[U+009B]31mforged`, the clone line prints the raw `C2 9B`, before and after this change. `--into` and `--local-remote-dir` go through the same `PATH_RE` (read in the code, not measured). That would be a follow-up for `checked_value`, which every flag shares.

Review: rv953, lane 5's independent reviewer, not the writer (w953): READY at `c42afa30`, the head that lands (issuecomment-6284626889), with no MUST. It rules #953's Verify met in substance: the refused run's only git call is the checkout probe every run makes, which names nothing of the value, and there is no clone. Its SHOULD is #459's documented C1 known limit (U031, L4-24), left with its test NIT for a follow-up on a ruling. CI (Brett Heap's ruling 27): H0 is the head c42afa3 itself, which passed tests, tests-git-bash and SonarCloud; main has touched none of its files since its base 5b41d44; and the local gate passed on its predicted squash tree e146bb5 over main e656199: the setup_project_repo_misdrawn, repo_hygiene, agents_md_codes, cli_reference sets, 192 passed. Landed on Brett Heap's word of 2026-10-18T14:45:28Z, relayed to every lane by lane 3 (#473 issuecomment-6251255596): "merge v1.2.0 PRs once reviewed and green".

Closes #953.

Lane: openreposhape-5 (openRepoShape-5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment