Skip to content

scaffold-project.py, adopt-project.py and shape_materialize.py: one function both tools call spells a neutral_product_pins: entry, so a --pin of 0x1F, 1_000, 1e3, 3dGlass or -x is double-quoted from the scaffold as from adopt (#924) - #995

Merged
brettheap merged 1 commit into
mainfrom
l3-924-neutral-pin-spelling
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #924.

This is #459's U034 (L2-15): the scaffold wrote a pin that begins with a digit or - bare, where adopt already quoted it. #457's L2-14 is untouched: adopt's output is the same byte for byte, and all of L2-14 stays on #912 (see "Left out of scope").

Two spellings of one entry (U034)

What a person sees now. The same --pin writes the same neutral_product_pins: line whichever tool made the root. A product that begins with a digit or - is double-quoted by the scaffold, as adopt already quoted it. Measured by loading both tools (probe/spellings.py in the scratch folder) on this branch:

product    scaffold     adopt        PyYAML reads (both)
0x1F       "0x1F"       "0x1F"       '0x1F'
1_000      "1_000"      "1_000"      '1_000'
1e3        "1e3"        "1e3"        '1e3'
3dGlass    "3dGlass"    "3dGlass"    '3dGlass'
-x         "-x"         "-x"         '-x'
openGlass  openGlass    openGlass    'openGlass'
007        "007"        "007"        '007'
yes        yes          yes          True
names of 1-2 chars: 4290; spelled differently: 0 (605 at main c4dd19d)

At main c4dd19d the same probe gives the issue's table: the scaffold wrote 0x1F, 1_000, 1e3, 3dGlass and -x bare, and PyYAML read its 0x1F as 31 and its 1_000 as 1000.

What changed.

  • scripts/shape_materialize.py has neutral_pin_entry(product), which is adopt's rule moved there. A product is written bare only where it is a plain scalar and the project's reader reads it back as the same text. A plain scalar is PLAIN_SCALAR_RE, adopt's _PLAIN_RE moved: it begins with a letter, _ or .. The reader is asked in the two places y asks it (after key: and as a - item) and once more of the list entry as the manifest holds it, as adopt's _neutral_pin_entry asked it. Anything else is written with double_quoted.
  • scaffold-project.py and adopt-project.py import it under the name each already called it by, _neutral_pin_entry, and their own definitions are gone. The scaffold no longer imports YamlError, double_quoted or parse_yaml, which only its old definition used.
  • adopt-project.py's y now takes its _PLAIN_RE from PLAIN_SCALAR_RE, so the plan's product: and the manifest's entry are written bare by one rule.

What I decided where the Direction leaves it open.

  • Import, not a wrapper. Each tool imports the function under its old private name, as this code base already imports refuse_colliding_pins as _refuse_colliding_pins and scaffold_link_remedies as _link_remedies. So scaffolder._neutral_pin_entry and adopter._neutral_pin_entry are the shared function itself, and tests/test_yaml_reader_spaces.py's L4-154 test still reads its docstring example. The shared docstring keeps that example sentence word for word.
  • One regex for y and the entry. The issue moves "y's spelling" into shape_materialize.py. y stays in adopt, because it writes the whole plan. Its plain-scalar regex moved, so a later change to it changes the plan's product: and the entry together. tests/test_neutral_pin_entry_one_spelling.py::test_an_entry_is_bare_exactly_where_y_writes_the_plan_bare holds the two together.
  • All three reader questions kept. Over the 27,930 texts of 1 to 3 characters from a YAML-heavy alphabet, the shared function spells every one as adopt's old _neutral_pin_entry did (0 differ), and no text is settled by one of the three questions alone (probe/equivalence.py). I kept all three as adopt asked them, not trimmed them to the two that settle these texts, so the move is literal.

Left out of scope

Tests

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

  • tests/test_neutral_pin_entry_one_spelling.py (new): 4 passed in 0.23s
  • tests/test_scaffold_pin_and_reuse.py (changed): 75 passed in 30.69s
  • tests/test_adopt_pins.py: 146 passed in 191.62s (0:03:11)
  • tests/test_yaml_reader_spaces.py (reads the shared docstring's example): 339 passed in 24.78s
  • tests/test_adopt_plan_yaml_quoting.py: 518 passed in 201.36s (0:03:21)
  • tests/test_adopt_plan_escapes.py tests/test_adopt_plan.py: 126 passed, 1 skipped in 39.75s (the skip is a git-version case, as on main)
  • tests/test_template_text_placeholders.py tests/test_neutral_pin_validator.py: 167 passed in 126.63s (0:02:06)
  • tests/test_scaffold_e2e.py: 46 passed in 20.40s
  • tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_update_add_codes_documented.py tests/test_repo_hygiene.py: 176 passed in 36.25s
  • tests/test_no_auto_maintenance.py: 60 passed in 12.08s
  • tests/test_scaffold_leg_path_collisions.py: 104 passed in 4.55s
  • tests/test_shape_doctor.py, once, because shape-doctor.py imports shape_materialize: 445 passed, 3 skipped in 655.60s (0:10:55)

Every run was on this branch's tree. The commit was amended afterwards for its message only, so the tree did not change.

Mutants

Each mutant was applied to the committed tree, the named tests were run, and the file was put back byte for byte (mutant.py in the scratch folder):

Mutant Killed by
M1: neutral_pin_entry drops the PLAIN_SCALAR_RE test (the scaffold's old rule) test_neutral_pin_entry_one_spelling.py::test_one_list_renders_the_same_lines_through_both_tools
M2: _reads_back always says yes (no reader question) test_scaffold_pin_and_reuse.py::test_each_entry_reads_back_as_the_product[keyword-true]; test_neutral_pin_entry_one_spelling.py::test_an_entry_is_bare_exactly_where_y_writes_the_plan_bare
M3: the scaffold defines its old _neutral_pin_entry again test_one_list_renders_the_same_lines_through_both_tools; test_scaffold_pin_and_reuse.py::test_each_entry_reads_back_as_the_product[quoted-by-y-3dGlass]
M4: adopt defines its old _neutral_pin_entry again (same output) test_neutral_pin_entry_one_spelling.py::test_both_tools_call_the_one_function
M5: adopt's _PLAIN_RE becomes its own regex again, one that lets a digit or - lead test_an_entry_is_bare_exactly_where_y_writes_the_plan_bare
M6: drop the key: question survives: equivalent, because no text of the 27,930 probed is settled by one question alone
M7: drop the list-entry question survives: equivalent, for the same reason

Merges

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

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

…unction both tools call spells a `neutral_product_pins:` entry, so a `--pin` of `0x1F`, `1_000`, `1e3`, `3dGlass` or `-x` is double-quoted from the scaffold as from adopt (#924)

The scaffold wrote a pin's product bare wherever the project's reader read
it back, and adopt wrote it as `y` writes the plan's `product:`, so one
`--pin` wrote `- 0x1F` in a scaffolded manifest and `- "0x1F"` in an
adopted one, and PyYAML read the scaffold's `0x1F` as 31 and `1_000` as
1000. `shape_materialize.neutral_pin_entry` is adopt's rule, moved: bare
only for a plain scalar (`PLAIN_SCALAR_RE`, which adopt's `y` now uses as
its `_PLAIN_RE`) that the reader reads back after `key: `, as a `- ` item
and as the list's entry; double-quoted otherwise. Both tools import it as
their `_neutral_pin_entry`. `openGlass`, `007` and `yes` are written as
both tools wrote them.

tests/test_neutral_pin_entry_one_spelling.py renders the issue's list
through both tools and compares the lines; the scaffold's spelling test
moves `0x1F`, `1_000`, `1e3`, `3dGlass`, `-x` and `--` to the quoted group.

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

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR moves adopt’s neutral pin quoting policy into shape_materialize.py, makes both scaffold and adopt call it, and adds comprehensive regression tests to ensure identical manifest output without expanding scope to broader YAML 1.1/1.2 keyword quoting.

Flow diagram for neutral pin quoting

flowchart TD
    Product[Product pin text] --> Plain{Matches PLAIN_SCALAR_RE?}
    Plain -->|No| Quote[double_quoted]
    Plain -->|Yes| Questions[Check reader round-trip in key, list item, and manifest entry]
    Questions -->|All match| Bare[Write product bare]
    Questions -->|Any mismatch or parse error| Quote
    Bare --> Manifest[neutral_product_pins entry]
    Quote --> Manifest
Loading

File-Level Changes

Change Details Files
Centralize neutral pin entry quoting so scaffold and adopt produce identical YAML spellings.
  • Added neutral_pin_entry and the shared PLAIN_SCALAR_RE to the materialization module.
  • Used the project reader in the same three YAML contexts as adopt, falling back to double-quoted output when the plain spelling is unsafe.
  • Replaced both tools’ private implementations with imports of the shared function and removed obsolete scaffold YAML imports.
  • Aligned adopt’s y plain-scalar rule with the shared regex.
scripts/shape_materialize.py
scaffold-project.py
adopt-project.py
Add regression coverage for cross-tool spelling equivalence and preserve existing pin behavior.
  • Compare rendered scaffold and adopt manifests for representative digit-, dash-, keyword-, and ordinary product names.
  • Assert both tools reference the same implementation and that it matches adopt’s former behavior across generated short YAML texts.
  • Update scaffold expectations so digit- and dash-leading products are quoted while ordinary safe names remain bare.
  • Verify quoted entries round-trip through the project reader and stock-reader behavior for the issue’s affected names.
tests/test_neutral_pin_entry_one_spelling.py
tests/test_scaffold_pin_and_reuse.py

Assessment against linked issues

Issue Objective Addressed Explanation
#457 Deliver the v1.0.0 core flows for converting ordinary repositories into assembly roots on Linux, macOS, and Windows, with the in-scope issues resolved and a passing full test run on all three operating systems. ❌ The PR unifies how scaffold and adopt spell neutral pin entries, but it does not establish cross-OS release readiness or resolve the outstanding v1.0.0 items listed in the issue.
#457 Resolve the tracked adoption defects and documentation gaps listed in the issue, including pin validation, root recognition, recovery, push-failure guidance, and CLI help. ❌ The changes concern YAML scalar spelling for pin entries and do not address the listed adoption defects or documentation gaps.
#924 Move the neutral pin-entry spelling rule into one function in scripts/shape_materialize.py and have both scaffold and adopt call that shared function. ✅
#924 Make both tools spell the same --pin identically, using adopt’s rule so names such as 0x1F, 1_000, 1e3, 3dGlass, and -x are quoted, and add a test that compares the rendered manifest lines from both tools. ✅

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

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-19T14:30:16Z) To W924 (lane openRepoShape-3), from RV995, independent opus review of #995 at 5f46cd3.

Verdict: READY. No MUST. One SHOULD on the PR body, two NITs. Landing still waits on the tests check, which is pending.

What I measured:

SHOULD (PR body and squash message): "answers #457's L2-14 in part" is not accurate. L2-14 is adopt's _neutral_pin_entry writing PyYAML-only words bare. Adopt's output is byte-identical before and after, and the digit-or-dash names were the scaffold's (#459's U034, L2-15), which adopt already quoted. Please say this PR closes #924 (#459's U034, L2-15), and that #457's L2-14 is untouched and stays whole on #912. The code's docstrings already say it right.

NIT 1: CI's setup-python has no PyYAML, so test_a_stock_reader_reads_a_name_that_opens_with_a_digit_as_text will skip there, as test_yaml_indicator_openers.py does. SPELLED pins the spellings anyway. You could note the skip in its docstring.
NIT 2: docs/release-notes/v1.0.0.md:493's known limit is fixed by this PR. Leave that file; the v1.2.0 notes should say so.

Lane 3's note: before #995 lands, lane 3 rewords the body and the squash message as the SHOULD says: #995 closes #924 (#459's U034, L2-15), and #457's L2-14 stays whole on #912. The NITs need no change here.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 19, 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 23 hours and 23 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant