Repository navigation
scripts/repo_shape.py: a block scalar's record carries its key and its body as a pair instead of NUL-joined text, so "a\x00b": | no longer reads as the key a with the body b<NUL>text, and a line that begins with a raw NUL and BLOCK no longer reads as a block scalar nobody wrote (#248) - #265
Conversation
…s body as a pair instead of NUL-joined text, so `"a\x00b": |` no longer reads as the key `a` with the body `b<NUL>text`, and a line that begins with a raw NUL and `BLOCK` no longer reads as a block scalar nobody wrote (#248) `_tokenise` kept a block scalar (`key: |`, `key: >`) as one `_Rec` whose text was `"\x00BLOCK\x00" + key + "\x00" + body`, and `_parse_mapping` took it apart with `split("\x00", 3)`. Since #199 the reader resolves `\x00`, `\u0000` and `\U00000000` in a double-quoted key, so a key holding a NUL was cut short at it and the rest of it read into the body; a raw NUL did the same before #199. And the marker was a text prefix: an ordinary line beginning with it was taken for a block record, read as a key and a value nobody wrote (`\x00BLOCK\x00k\x00v: 1` as `{'k': 'v: 1'}`), or, as a sequence item with one NUL too few, died with a bare `ValueError` that `load_yaml` let through as a traceback. `_Rec` gains a `block` slot holding `(key, body)`, set only on the record a block scalar's header makes, and `_parse_mapping` and `_parse_sequence` test that slot instead of a text prefix. `_BLOCK_SENTINEL` is gone. No character a key can hold can collide with a structure that is not a string. A NUL in a key is read, not refused: YAML allows it escaped and PyYAML 6.0.3 reads every escaped row here the same way, and the reader already took the same key over a plain value. A raw NUL, which YAML forbids, is read as the reader reads it in any other key; refusing raw control characters is a reader-wide decision outside this path. - `tests/test_yaml_block_scalar_keys.py` (new, 51 tests). Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-5, 2026-10-11T22:46Z) @codex review |
|
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 56 minutes by commenting |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Reviewer's GuideThe PR eliminates the collision-prone NUL-delimited representation of block scalars by storing their key and body as a structured pair on Sequence diagram for parsing a YAML block scalar with a NUL keysequenceDiagram
participant Input as YAML input
participant Tokenise as _tokenise
participant Rec as _Rec
participant Mapping as _parse_mapping
Input->>Tokenise: _tokenise(text)
Tokenise->>Tokenise: _opens_block_scalar(kv)
Tokenise->>Tokenise: _block_scalar_body(raw_lines, i, indent)
Tokenise->>Tokenise: _block_scalar_text(kv[1], body)
Tokenise->>Rec: create with block=(kv[0], body)
Note over Rec: text is empty, key and body remain separate
Tokenise-->>Mapping: list[_Rec]
Mapping->>Rec: read rec.block
Rec-->>Mapping: key, body
Mapping-->>Input: {key: body}
Flow diagram for avoiding block scalar marker collisionsflowchart LR
A[YAML block scalar header] --> B[_tokenise]
B --> C[_Rec.block = key, body]
C --> D{rec.block is not None}
D -->|yes| E[_parse_mapping uses key and body]
D -->|no| F[Parse ordinary text normally]
G[Key or line contains NUL or marker-like text] --> F
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…, #252 via #257, #195 via #226, #210 via #219, #211 via #220, #248 via #265, #212 via #223) into fix/adopt-record-submodule-placement Brings in 06b5204 (#214), 60d08a1 (#217), 2727b08 (#209), 4b2add8 (#252), aaa40eb (#195), d89fffa (#210), eb6df75 (#211), c3f4987 (#248) and 5751460 (#212) on top of 39ac59c. No rebase. 5751460 landed while the merge of c3f4987 was being checked, and it changes adopt-project.py, scripts/shape_materialize.py and tests/test_adopt_submodules.py, as this branch does, so this one merge takes it too. No conflicts. Both sides change adopt-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, tests/test_adopt_submodules.py and tests/test_shape_doctor.py. Every top-level definition only one side changed is that side's text in the merge, byte for byte. The one thing both changed is shape_materialize.py's imports, and both are kept: main's `codecs` and this branch's `path_spelling_fault` and `PATH_*` codes. Checked: - adopt-project.py: main calls `escape_what_stdout_cannot_encode()` first in `main` (#214). It makes a `.gitmodules` git's submodule reader cannot read a finding where the tip holds no submodule and the history does, or where `ignore`, `update` or `branch` has no value (#195). And every path a finding names is now `_spelled` (#212). This branch's `submodule_record`, `_resolution_text` and the record's preamble are unchanged by the merge, and so are `_answered_legs`, `_kept_in_a_leg`, `_commit_the_split`, `_refuse_what_check_finds`, `cmd_execute` and `Plan`. The record and #195's finding read the kept submodules the same way, `_kept_in_a_leg(_answered_legs(...))` over the tip's gitlinks. `execute` refuses #195's finding in `_refuse_what_check_finds`, before any leg exists and before `_commit_the_split` writes the record. So a source #195 refuses gets no record, and so does a source whose tip holds no submodule. - scripts/repo_shape.py: main refuses an explicit key in a flow mapping and reads an empty flow-mapping value as null (#252). A block scalar's record is now a (key, body) pair, and `_BLOCK_SENTINEL` is gone (#248); nothing on this branch names it. The record is a block sequence of double-quoted scalars, and it reads back through the merged reader with 0 mismatches (`submodule_record`, then `parse_yaml`, path and resolution): - 4,525 names, plus 35 more; - 240 aimed at #252 and #248 (`? a`, `{a: }`, `|`, `a: |`, a NUL, the old sentinel). - scripts/shape_materialize.py: main adds `escape_what_stdout_cannot_encode`, `run()` reading UTF-8 (#217), and `first_and_more(paths, spell)` (#212). This branch's `LEG_SEGMENT_REASONS` and `_spelling_reason` are untouched. - shape-doctor.py: main adds the `manifest kinds` finding for a `family.yaml` beside `project.yaml` (#211), and the stdout escaping. This branch's placement row and `recorded_submodules` are untouched. - tests: #212 respells the finding text that the earlier tests in test_adopt_submodules.py assert. None of this branch's tests quotes a finding. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, shape-doctor.py, validate-manifest.py or update-shape.py. Each merged module's names are the base's, plus what either side added, minus what either side removed. No side added a name the other side also added, in the tests either. - AGENTS.md is untouched by main: 472 lines, under the cap of 472. - The branch's diff against main is still #189 alone: 12 files, +1670 -156, file by file the same as against 39ac59c. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, #209, #252, #195, #210, #211, #248, #212, #251 and #206 merge with one hunk, the materializer import list, kept on both sides, and `_entry_findings` still hands `_leg_findings` the tree and ends with the root-symlink findings origin/main moved 39ac59c -> 60588d5: 06b5204 (#214 via #246), 60d08a1 (#217 via #230), 2727b08 (#209 via #218), 4b2add8 (#252 via #257), aaa40eb (#195 via #226), d89fffa (#210 via #219), eb6df75 (#211 via #220), c3f4987 (#248 via #265), 5751460 (#212 via #223), 23e16ae (#251 via #271) and 60588d5 (#206; its subject names no pull request). Conflict, resolved by keeping both sides: - adopt-project.py, the `shape_materialize` import list. This branch adds BOOTSTRAP, SYMLINK_HOPS and template_files; #214 adds escape_what_stdout_cannot_encode. The resolution is the union. Auto-merged: scripts/shape_materialize.py, where #214's escaping, #217's UTF-8 `run()` and #212's `first_and_more(paths, spell=None)` sit apart from this branch's #183 section, and adopt-project.py's other hunks. #195 adds `import os` at the line where this branch added it, and the merge keeps it once. Checked on the merged tree: - `_entry_findings` still calls `_leg_findings(plan, tree)` and ends with `_root_symlink_findings(plan, source, tree)`. `main` still calls `escape_what_stdout_cannot_encode()` first. - No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, and none is loaded but bound nowhere (AST). `first_and_more` gives its new `spell` a default, so this branch's call is unchanged. - Both sides are intact by +/- line sets: each side's change against the base reappears against the other side, apart from the import hunk and the shared `import os`. - #212 says this branch's `plan-root-symlink` names its paths raw, and that whichever pull request lands second should spell them. That was 04792d2. Since d11cf9f every path the finding names goes through `_spelled`. One clash the textual merge does not show is fixed in the next commit: #217's tests/test_git_output_decoding.py refuses a shipped text-mode `subprocess` call that names no codec, and this branch's `_checks_out_symlinks` probe names none. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-notice It merged with no conflict. Main brings nine commits since 39ac59c: - 06b5204, six tools escape a name their stdout cannot encode (#214 via #246), with tests/test_stdout_encoding.py; - 60d08a1, `shape_materialize.run()` and 26 other git and gh calls read the child's output as UTF-8 (#217 via #230), with tests/test_git_output_decoding.py; - 2727b08, update-shape.py's `gh pr create --base` (#209 via #218), with tests/test_update_shape_pr_base.py; - 4b2add8, scripts/repo_shape.py's flow mappings (#252 via #257), with tests/test_yaml_flow_mappings.py; - aaa40eb, adopt-project.py refuses a `.gitmodules` the history's gitlinks make unreadable (#195 via #226), with tests/test_adopt_history_gitlinks.py; - d89fffa, family.py `bump` to the current pin (#210 via #219); - eb6df75, shape-doctor.py's family.yaml beside a project.yaml (#211 via #220); - c3f4987, scripts/repo_shape.py's block-scalar records (#248 via #265), with tests/test_yaml_block_scalar_keys.py; - 5751460, adopt-project.py spells every path a FINDING or a refusal names (#212 via #223), with tests/test_adopt_finding_spelling.py. Three files changed on both sides: adopt-project.py, tests/test_adopt_ignored_shape_files.py and tests/test_adopt_verify.py. In each, `git diff -U0` of this merge against 5a8b649 equals main's diff against 39ac59c, and this merge against 5751460 equals the branch's diff against 39ac59c, apart from the hunk headers. Neither side's lines are changed. How main's changes meet this branch's failure handling: - Every definition the late failures go through is byte-identical to 5a8b649's: LATE_FAILURES, the notice (`_nothing_rolled_back`), `_failed_loudly`, `_report_late`, the three builders' handlers, `_verify` and `cmd_execute` among them. Three definitions differ, each by main's hunk alone. - `main()` gains `escape_what_stdout_cannot_encode()` as its first statement (06b5204). The branch writes every REFUSED block and notice to stderr, which Python opens with `backslashreplace` whatever the locale. - `run()` names its codec (60d08a1). Output that is not UTF-8 now reaches the branch's handlers as `CommandFailed`, not as `UnicodeDecodeError`. LATE_FAILURES keeps `UnicodeError`, because `git_out` in scripts/repo_shape.py still decodes strictly. - `Source.registered_submodules` reads the history (aaa40eb). Its one caller in `execute` is `_refuse_what_check_finds`, before `_create_leg_remotes`, so the new refusal comes before any leg repository exists and needs no notice. - 5751460 spells the paths of the verification's findings. The branch's footer and notice follow those findings unchanged. No top-level name is bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py or scripts/shape_materialize.py, and a `symtable` check finds no name they read that nothing binds. On the merged tree, ten test files give 439 passed and 1 skipped (Windows' own stdout default): the two shared ones, main's four new ones that read adopt-project.py (finding_spelling, history_gitlinks, stdout_encoding, git_output_decoding), and test_adopt_late_failure, test_adopt_filter_repo_failure, test_adopt_execute_refusals and test_adopt_plan_escapes. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-5 (openRepoShape-5)
Claim: #248 (comment)
What
The YAML reader kept a block scalar (
key: |,key: >) as one_Recwhose text was"\x00BLOCK\x00" + key + "\x00" + body._parse_mappingtook it apart withsplit("\x00", 3), and both_parse_mappingand_parse_sequencerecognised it by that text prefix. Now:_Rechas ablockslot holding(key, body). It is set only on the record a block scalar's header makes, and that record'stextis empty._tokenisebuilds the record with the pair._parse_mappingand_parse_sequencetestrec.block, not a text prefix._BLOCK_SENTINELis gone. Its comment, which named the collision as a known limit, becomes a_Recdocstring saying why nothing can collide now.No other function in the file changes, and no writer changes.
Why
Since #199 the reader resolves
\x00,\u0000and\U00000000in a double-quoted key, which is valid YAML. A key holding a NUL was cut short at the NUL, and the rest of it read into the body. A raw NUL did the same before #199. The marker was also only a text prefix, so an ordinary line beginning with a raw NUL +BLOCK+ NUL was taken for a block record:"a\x00b": |{'a': 'b\x00text\n'}{'a\x00b': 'text\n'}(PyYAML agrees)"a\x00b": text(control){'a\x00b': 'text'}<NUL>BLOCK<NUL>k<NUL>v: 1{'k': 'v: 1'}{'\x00BLOCK\x00k\x00v': 1}- <NUL>BLOCK<NUL>xValueErrorout ofload_yaml['\x00BLOCK\x00x']k:\n <NUL>BLOCK<NUL>x<NUL>y{'k': {'x': 'y'}}YamlError, ask:\n xyzisA structure that is not a string has no character to collide with, so this removes the class and not just
\x00. A different separator only moves the collision, and refusing a NUL in a block key leaves the prefix misreads in place (a\x01separator and a NUL refusal were each tried as mutants, and both are caught by the new tests).A NUL in a key is read, not refused. YAML allows it escaped. PyYAML 6.0.3 reads every escaped row the way this branch does. The reader already read the same key over a plain value, and
double_quotedwrites a NUL as\x00. A raw NUL, which YAML forbids, is read the way the reader reads a raw NUL in any other key (a<NUL>b: textreads as{'a\x00b': 'text'}on main). Refusing raw control characters is a reader-wide decision and is left out (see below).Issue row 2 (
"a\^@b": >) does not reproduce, not even on a02240a:\^@is no escape the reader (or YAML) resolves, so the key keeps its backslash on main and here. It is pinned as a control.Evidence
tests/test_yaml_block_scalar_keys.py(new, 51 tests). On 150c41d: 41 fail and 10 pass, and the 10 that pass are controls. On this branch: 51 pass.Targeted runs on 9be6287:
test_yaml_flow_quotes.py,test_load_yaml_refusals.py,test_adopt_plan_escapes.py,test_manifest_validator.py,test_pin_validator.py,test_neutral_pin_validator.py,test_repo_hygiene.py,test_cli_reference.pyandtest_adopt_plan.py: 326 passed, 1 skipped (SHAPE_MEDXEHR_CLONEunset);test_shape_doctor.py: 181 passed.Mutation: each fix hunk reverted on its own (
_tokenise,_parse_sequence,_parse_mapping,_Rec) fails 46, 4, 48 and 51 tests. Of the narrow alternatives, refusing a NUL in a block key fails 41, including every marker-prefixed-line test. A\x01separator fails the every-character round-trip test. Dropping the sequence check fails the placement test, and taking the pair reversed fails 46.Every code point in a written block key,
double_quoted("a" + chr(c) + "b")under|and>: all 2,228,224 documents read back, and PyYAML agrees on every one. Main misreads exactly 2 of them, both NUL.A base-against-head fuzz of 200,000 documents built around block scalars finds 0 differences outside the class. All 23,298 documents with no NUL in any spelling read the same. Over the same corpus, main lets 4,250 bare
ValueErrors out ofparse_yaml, and this branch none.Reach: all 14 tracked YAML files read the same on main and here, rendered templates too. Block scalars in
tests.yml,path-classification.yamland bothvalidate.ymltemplates read unchanged. No writer in this repository emits the construct:adopt-project.py's keys are literals, and its one variable key comes fromsummary_of's fixed names;ynever writes|,>or a raw NUL plain;y/emit, andfamily.py,update-shape.pyandbump-leg.pywrite literal keys.So the defect shows only on a file a person edits or another tool writes. Every YAML file every tool reads goes through this reader.
Composition
git merge-tree --write-treefrom 9be6287:planraisesUnicodeEncodeErrorin its terminal report, after the plan file is written, for a printable name stdout cannot encode (日本.mdunder cp1252, as on a redirected Windows stdout), soplanexits 1 beforeplan written to#214 and shape_materialize.run() and 26 other git and gh calls read what the child wrote as UTF-8 whatever the locale, so anadopt-project.py executewhose leg push is rejected with a message naming a non-ASCII path endsREFUSED pushing the spec legand the ruleset hint underLC_ALL=C PYTHONUTF8=0, not a UnicodeDecodeError traceback (#217) #230): clean.fix/yaml-quote-boundaries(scripts/repo_shape.py treats a quote character in the MIDDLE of a plain scalar as opening a quote, sok: [it's, a]reads["it's, a"]where its block twin reads["it's", "a"],k: it's fine # notekeeps the comment in the value, and #244 turnsk: [a"b\", c]from right (by accident) into one item #249/scripts/repo_shape.py_scalartakes any text that starts and ends with a quote for a quoted scalar and anything else for plain text, so an unterminated"abcreads as"abc,- "a\"reads asa\, and"a" b "c"reads asa" b "c, where PyYAML refuses each #250) at 21fc213: clean; the review also passed 363 tests on this branch merged with it and scripts/repo_shape.py reads? a: bin a flow mapping as the literal key? a, where the block reader refuses explicit keys, and refuses{a: }and{a:}asnot a flow collection: ''instead of reading{'a': None}#252 together.fix/yaml-flow-mapping-keys(scripts/repo_shape.py reads? a: bin a flow mapping as the literal key? a, where the block reader refuses explicit keys, and refuses{a: }and{a:}asnot a flow collection: ''instead of reading{'a': None}#252) at 2438190: clean. The merged tree passes this file,test_yaml_flow_quotes.pyandtest_yaml_flow_mappings.py(114 passed).fix/scaffold-referent-chain-links(scaffold-project.py--referent-chainwrites each middle link into project.yaml as typed (naming_block,scripts/shape_materialize.py:1010-1011) and checks none, so a link holding#scaffolds all three repositories, exits 0, and pushes a manifest the project's own validators refuse asyaml-unparsable, and a link holding'or]is recorded as a different chain #251) at 114bec9: clean.Left out
unacceptable character #x0000). The reader takes them everywhere today, anddouble_quoted's docstring says so. A refusal belongs to the whole reader, not to one path. The plain-twin tests here say those rows become refusals together if that changes. A follow-up issue is proposed.+chomping and the indentation digit is documented as accepted, not honoured, and is unrelated.Closes #248
🤖 Generated with Claude Code
Summary by Sourcery
Make block-scalar parsing preserve arbitrary keys and distinguish genuine block records from ordinary YAML lines.
Bug Fixes:
Enhancements:
Tests: