Repository navigation
adopt-project.py check and execute print a source path raw in a FINDING, so a \r, \n, \x85, U+2028 or ESC in a name overwrites, splits or escapes the line on a terminal, where plan's questions spell the same path with _spelled #212
Description
Lane: openreposhape-5 (openRepoShape-5)
Found by this lane while fixing #199, whose writer left it out of that fix on purpose. Pre-existing on main e4decfb.
What is wrong. Every FINDING that names a path prints it RAW, so a source name holding a character a terminal acts on changes what the line shows. plan spells the same names in every question it writes (_spelled, adopt-project.py:675-697: \r, \n, \x85, \u2028, \x1b and every other non-printable character, plus a backslash and a backtick, escaped so the spelling reads one way only), and #166's round 4 spells every path a question names. The findings were not given it. execute prints the same strings: its refusal is check's own list, "as check prints them" (_refuse_what_check_finds).
What each character does to the line:
\rreturns the cursor to column 0, so the text after it overwrites the code and the path before it. A person readingcheckseesis in the source tree and in no plan entryand not which path.\nends the line. One FINDING prints as two lines and the second no longer starts withFINDING, sogrep '^FINDING', a CI annotator, or anything reading one finding per line, misses half of it.\x85, U+2028 and U+2029 are line boundaries tostr.splitlines, and to any viewer that honours them, the same way.- ESC starts a terminal escape sequence out of a file name the adopted repository's author chose. A name
a\x1b[2Kbprints the ANSI "erase line" sequence.
SHAPE=<openRepoShape checkout>; TMP=$(mktemp -d)
GIT='git -c user.name=T -c user.email=t@example.invalid'
git init -q -b main "$TMP/src" && cd "$TMP/src"
printf '# Thing\n' > README.md && git add -A && $GIT commit -qm init
python3 "$SHAPE/adopt-project.py" plan --source "$TMP/src" --project Northwind --org testorg \
--elected-by 'T H' --elected-on 2026-09-02 --out "$TMP/plan.yaml" >/dev/null # exit 0
# 1. an ENTRY path. Main's reader resolves `\n`, so a hand-written entry can hold a newline.
sed 's/^ - path: README.md$/ - path: "README\\nX.md"/' "$TMP/plan.yaml" > "$TMP/plan-nl.yaml"
python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan-nl.yaml" 2>&1 >/dev/null \
| grep -a -v -e '^$' -e 'finding(s) in' | cat -A
# 2. a TREE path. A commit after the plan adds a file named `Icon<CR>`.
printf 'x\n' > "$(printf 'Icon\r')" && git add -A && $GIT commit -qm 'add Icon<CR>'
python3 "$SHAPE/adopt-project.py" check --plan "$TMP/plan.yaml" 2>&1 >/dev/null | grep -a plan-uncovered | od -cObserved on main e4decfb:
FINDING plan-uncovered: README.md is in the source tree and in no plan entry$
FINDING plan-empty-entry: README$
X.md covers nothing in the source tree at this commit$
0000000 F I N D I N G p l a n - u n c
0000020 o v e r e d : I c o n \r i s
0000040 i n t h e s o u r c e t
0000060 r e e a n d i n n o p l
0000100 a n e n t r y \n
Route 1 is one finding on one entry printed as two physical lines. Route 2 shows the raw carriage return (\r in the od -c dump) that a terminal turns into the overwrite above. A source holding seven odd names (Icon\r, a\tb, a\nb, a\x1b[2Kb, a\x85b, a\u2028b, a\u2029b) added after the plan prints all seven raw in check. execute --yes against the same plan, with source.commit set to the current head so it is not plan-stale, exits 2 with REFUSED plan-uncovered: 7 finding(s) in <plan>, as `check` prints them: and the same seven raw lines; nothing is created.
plan cannot write an entry for \r on main (#199), which is why the routes above use a hand-written \n entry and a tree path. With #199's fix (branch fix/adopt-plan-linebreak-names, d3023fb) it can, so the plain flow reaches this: plan on a source tracking Icon\r, then check, prints FINDING plan-unresolved: Icon\r still has `leg: null`. The question was: ... with the raw CR. macOS writes that file into every folder given a custom icon (#199).
Expected. A path in a FINDING is printed as _spelled prints it in a question: FINDING plan-uncovered: `Icon\r` is in the source tree and in no plan entry. A FINDING is one line, and check's output holds no character str.isprintable() refuses other than the newline that ends each line.
Where. adopt-project.py on main e4decfb, each f-string takes path as it is:
_coverage_findings:plan-duplicate-path:1542,plan-uncovered:1552,plan-covered-twice:1558,plan-empty-entry:1562._leg_findings:plan-unresolved:1587,plan-bad-leg:1591._as_entry:1661-1663, which the three submodule findings use (:1738, :1813, :1820)._account_for:adopt-duplicated:2782 andadopt-lost:2787, the verificationexecuteprints after the split.- Printed by
cmd_check:1944 and, through_entry_findings, by_refuse_what_check_finds:2319.
A test would put each of the five characters above in a path under each finding code and assert the output.
Coordination. Lane openRepoShape-3's #196 rewrites _entry_findings, so this is most likely fixed in that PR or straight after it. Its branch fix/adopt-check-unsafe-path (a WIP at 30badde when this was written) adds a plan-unsafe-path finding that already spells its path with _spelled, while the findings of _coverage_findings and _leg_findings listed above are unchanged on it (read with git show origin/fix/adopt-check-unsafe-path:adopt-project.py). Either one change to the f-strings above, or one helper every finding takes its path through so the next finding cannot print one raw. Talk to that lane before starting.
Not claimed.
Activity
brettheap commentedon Oct 11, 2026
CLAIMED — lane openRepoShape-3, session 566fec94-6fde-4579-947f-ec4fda5ce14f@Eagle, 2026-10-11T16:15:21Z, for #212
Logged in
lanes/log/openRepoShape-3.mdataf7be9765b011b85d02c92c8affbde3d9c623c95.The three reads (lane-collision-protocol Rule 1):
adopt-project.py: #173's `adopt-root-submodule` and `adopt-unexpected…
adopt-project.py: `adopt-mount-occupied` keeps a byte that is not UTF…
fix round 1 (#199): the question list that ends `plan`'s report is te…
adopt-project.py: the plan writer escapes every character that is not…
fix round 2 (#212): the carriage-return case of #247's plan-escapes t…
fix round 3 (#212): `first_and_more` takes an optional `spell`, so `_…
adopt-project.py: `check` and `execute` spell every path a FINDING or…
fix round 3 (#196): the seven assertions #212's spelling of finding p…
Merge origin/main (5751460: #214 via #246, #217 via #230, #209 via #218…
adopt-project.py: `check` and `execute` judge an entry's path by one …
Merge origin/main 60588d5 into fix/adopt-root-kept-symlinks: #214, #217…
shape_materialize.py: the materializer's refusals name each path betw…
Merge origin/main (5751460, #212 via #223) into fix/adopt-late-failur…
adopt-project.py: the late-failure notice, `adopt-work-dir-occupied` …
Merge origin/main (60588d5) into fix/adopt-plan-quote-yaml-keywords
Merge origin/main (d94d155) into fix/adopt-plan-quote-yaml-keywords
tests: #190's relative-url finding is expected with the submodule and…
adopt-project.py: `check` finds, and `execute` refuses before any leg…
adopt-project.py: every failure once a leg repository exists flushes …
Merge origin/main 7a8b981 into fix/adopt-root-kept-symlinks: #189's `…
adopt-project.py: `check` and `execute` spell a plan's hand-edited `q…
adopt-project.py: `check` follows each symlink the plan keeps where t…
tests.yml and adopt-project.py: the Windows job installs git-filter-r…