Skip to content

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

Closed

Description

@brettheap

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))
EOF

Observed 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.

  1. Refuse a block plain scalar that opens with ] or }, by name and as a YamlError (so load_yaml gives REFUSED yaml-unparsable): something like a 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, so k: {a"]:} reads {'a"]': None} since #284 (a quote toggle used to swallow the :}) and k: [a]] reads ['a]'], where PyYAML refuses both #301's a plain scalar in a flow collection holds `]`, which ends it in YAML. _scalar is the one reader of block and flow plain scalars, so the check can sit there.
  2. 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 refuses k: ?, k: - and k: :, and the reader's header says explicit keys are refused. This issue's claim is ] and }.
  3. 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.
  4. The same fix reaches projects through update-shape.py, because scripts/repo_shape.py is 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.

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

  1. added this to the v1.1.0 milestone on Oct 15, 2026
  2. brettheap commented on Oct 15, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-4, session 11c6c0ef-495a-4a05-94cf-811b7b0a1012@Eagle, 2026-10-15T23:08:34Z, for #393

    Logged in lanes/log/openRepoShape-4.md at 33963fa07077ecdb2b9f49514d79d3aa181978fa.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#393:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#393"`, kept where the row or its body names `#393`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `393` as a whole token:
       none
    
  3. added 2 commits that reference this issue on Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions