Repository navigation
repo_shape.py: _scalar returns a BLOCK plain scalar that opens with ] or } as text, so k: ], k: } and - ] read as the strings ']' and '}', where YAML forbids a plain scalar to open with a flow indicator and PyYAML refuses, and the same fall-through reads k: ,, k: @a, `k: a and k: %a` #393
Description
Lane: openreposhape-5 (openRepoShape-5)
(lane openRepoShape-5, 2026-10-12T15:21:17Z)
Found by lane 5's review of fix/yaml-flow-brackets-surrogates at 21fbea4 (the fix for #301), as its F3. #301 and that fix are about a bracket in a FLOW collection; this is the same stray closer one context over, in a block value, and it was outside #301's class on purpose. Pre-existing: the same read on c7c02fa, on origin/main d82a83e and on 21fbea4 (every row below checked on both main and 21fbea4, identical). It is 5,611 of the 15,131 documents that the review's seeded structured fuzz (seed 7301280, 600,000 documents, PyYAML 6.0.3 as the oracle only) still saw read on 21fbea4 where PyYAML refuses, the largest single class. Reproduced at 2026-10-12T15:12Z. No repository, no network and no gh: the reader is a pure function. Not claimed.
What is wrong. scripts/repo_shape.py _scalar (:445) reads every block plain scalar: _parse_mapping calls it for a mapping value (:783, out[key] = _scalar(rest)) and _parse_sequence for a sequence item (:738). After the empty, quoted and number checks, its only test of the first character is if text[0] in "&*!" (:451), which refuses anchors, aliases and tags. Everything else falls to return text (:461).
In YAML a plain scalar cannot open with an indicator: , [ ] { } # & * ! | > ' " % @ and a backtick, and ? : - only when a non-space character follows. A [ or { opening a block value is a flow collection and reaches _flow. A ] or } opens nothing: it closes a collection that was never opened, so k: ] is not a document. This reader returns the string ']'.
The same fall-through also reads a value opening with ,, @, a backtick or %, and a lone ?, - or :; each is in the rows below and PyYAML refuses each. They go through the same line, and a fix that tests the first character once covers them.
Since #301's fix the two contexts disagree: a plain scalar in a FLOW collection that holds a bracket anywhere is refused (k: [a, ]x]), and a plain scalar in a BLOCK value may open with a closer. A bracket elsewhere in a block plain scalar is valid YAML and reads as it should (k: a], k: x}, k: a[0], - see [1]: controls below).
cd <an openRepoShape checkout at d82a83e>
python3 - <<'EOF'
import sys
sys.path.insert(0, "scripts") # run from the repository root
from repo_shape import YamlError, parse_yaml
try:
import yaml # PyYAML, as the oracle only
except ImportError:
yaml = None
DOCS = [
"k: ]",
"k: }",
"- ]",
"- }",
"k:\n j: ]",
"k: ]x",
"k: ] a",
# the neighbours: other characters YAML bars from opening a plain scalar
"k: ,",
"k: @a",
"k: `a`",
"k: %a",
"k: ?",
"k: -",
"k: :",
# controls: a closer that does not open the scalar is plain text
"k: a]",
"k: x}",
"- see [1]",
'k: "]"', # a quoted scalar may hold anything
]
def ours(doc):
try:
return repr(parse_yaml(doc + "\n"))
except YamlError as e:
return f"refused: {e}"
def pyyaml(doc):
try:
return repr(yaml.safe_load(doc + "\n"))
except yaml.YAMLError as e:
return f"refused: {type(e).__name__}"
for doc in DOCS:
print(repr(doc))
print(" repo_shape:", ours(doc))
if yaml:
print(" PyYAML: ", pyyaml(doc))
EOFObserved on origin/main d82a83e (Python 3.12.3, PyYAML 6.0.3, Linux), the same on 21fbea4:
'k: ]'
repo_shape: {'k': ']'}
PyYAML: refused: ParserError
'k: }'
repo_shape: {'k': '}'}
PyYAML: refused: ParserError
'- ]'
repo_shape: [']']
PyYAML: refused: ParserError
'- }'
repo_shape: ['}']
PyYAML: refused: ParserError
'k:\n j: ]'
repo_shape: {'k': {'j': ']'}}
PyYAML: refused: ParserError
'k: ]x'
repo_shape: {'k': ']x'}
PyYAML: refused: ParserError
'k: ] a'
repo_shape: {'k': '] a'}
PyYAML: refused: ParserError
'k: ,'
repo_shape: {'k': ','}
PyYAML: refused: ParserError
'k: @a'
repo_shape: {'k': '@a'}
PyYAML: refused: ScannerError
'k: `a`'
repo_shape: {'k': '`a`'}
PyYAML: refused: ScannerError
'k: %a'
repo_shape: {'k': '%a'}
PyYAML: refused: ScannerError
'k: ?'
repo_shape: {'k': '?'}
PyYAML: refused: ScannerError
'k: -'
repo_shape: {'k': '-'}
PyYAML: refused: ScannerError
'k: :'
repo_shape: {'k': ':'}
PyYAML: refused: ScannerError
'k: a]'
repo_shape: {'k': 'a]'}
PyYAML: {'k': 'a]'}
'k: x}'
repo_shape: {'k': 'x}'}
PyYAML: {'k': 'x}'}
'- see [1]'
repo_shape: ['see [1]']
PyYAML: ['see [1]']
'k: "]"'
repo_shape: {'k': ']'}
PyYAML: {'k': ']'}
Why it matters. YamlError's docstring is the reader's contract: "Raising rather than guessing is the whole point: a validator that silently mis-reads its own contract file is worse than one that will not start." A manifest, pin, plan or family file with a stray ] or } left as a value (a half-deleted flow list, a truncated edit) reads as data (tracking_branch: ] reads as the string ']'), where PyYAML refuses the file, so the project's validators and a stock parser disagree about what it says. Nothing in this tree writes one. The adopt plan's y() leaves a value plain only when it opens with a letter, _ or . (adopt-project.py:252, _PLAIN_RE), and the scaffold refuses a value for a bare template placeholder that is not ^[A-Za-z0-9._/@+~-]{1,255}$ (scaffold-project.py --tracking-branch ']x' is refused unsafe-value on d82a83e). So only a hand edit reaches it, as with #301, which is why it is not urgent.
What it should do.
- Refuse a block plain scalar that opens with
]or}, by name and as aYamlError(soload_yamlgivesREFUSED yaml-unparsable): something likea plain scalar cannot open with `]`: ']', the wording the author's choice, in the voice of repo_shape.py: a flow collection with an unmatched]or}is read, not refused, sok: {a"]:}reads{'a"]': None}since #284 (a quote toggle used to swallow the:}) andk: [a]]reads['a]'], where PyYAML refuses both #301'sa plain scalar in a flow collection holds `]`, which ends it in YAML._scalaris the one reader of block and flow plain scalars, so the check can sit there. - The neighbours in the same test, if the author takes them:
,,@, a backtick and%(each a reserved or structural indicator YAML bars from opening a plain scalar, and each refused by PyYAML). A lone?,-or:is a case for the author to rule on: PyYAML refusesk: ?,k: -andk: :, and the reader's header says explicit keys are refused. This issue's claim is]and}. - Keep reading what YAML reads: a closer or other indicator that does not OPEN the scalar (
k: a],k: x},k: a[0],- see [1],k: a, b), and every quoted scalar (k: "]",k: ']'), exactly as before. - The same fix reaches projects through
update-shape.py, becausescripts/repo_shape.pyis the digest-pinned copy in every scaffolded root.
How to test. A table in the reader's test files, one row per document above with a PyYAML-equal control for each still-read row: tests/test_yaml_flow_brackets.py once the #301 fix lands (its STILL_READ list already pins the controls and says each equals PyYAML), or a new file beside tests/test_yaml_block_scalar_keys.py. Assert YamlError (and, through load_yaml on a file, REFUSED yaml-unparsable naming the file) for k: ], k: }, - ], - }, k:\n j: ], k: ]x and k: ] a, and the unchanged read for the controls. Reverting the check fails the refusal rows. A reader change runs tests/test_load_yaml_refusals.py, tests/test_yaml_quote_boundaries.py, tests/test_yaml_flow_quotes.py, tests/test_yaml_flow_mappings.py and tests/test_manifest_validator.py as well, because they pin what else the reader reads.
Not in this issue.
- A bracket in a FLOW collection: repo_shape.py: a flow collection with an unmatched
]or}is read, not refused, sok: {a"]:}reads{'a"]': None}since #284 (a quote toggle used to swallow the:}) andk: [a]]reads['a]'], where PyYAML refuses both #301 and its fix. - Empty flow entries, entries opening with
:,#,?or-in a flow collection, and a single-pair mapping in a flow sequence: a separate issue. - Refusals of valid YAML (
k: ["a": b],k: {? a: b}, which PyYAML reads): the reader refuses explicit keys by design (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), and["a": b]is a refusal for another reason. k: !, which this reader refuses as a tag and PyYAML reads as a null (the opposite direction).- Whether a stray closer should be named with a line number: the block reader's errors carry
line N:where they have one, so the author may add it.
Related: #301 (the flow-context half, and the sketch it gives); #252 and #249 (the explicit-key and bracket-in-key refusals, each a rule of the same kind); the follow-up for flow entries, #392.
Activity
brettheap commentedon Oct 15, 2026
CLAIMED — lane openRepoShape-4, session 11c6c0ef-495a-4a05-94cf-811b7b0a1012@Eagle, 2026-10-15T23:08:34Z, for #393
Logged in
lanes/log/openRepoShape-4.mdat33963fa07077ecdb2b9f49514d79d3aa181978fa.The three reads (lane-collision-protocol Rule 1):
docs/release-notes/v1.1.0.md: refreshed at main 4cf5fb0 (49 pull requ…
repo_shape.py: #635 fix round 1: the YAML fuzz leaves out the empty b…
repo_shape.py, shape_materialize.py and adopt-project.py: a value the…
repo_shape.py strips only YAML's space and tab, so a value a Unicode …