Repository navigation
repo_shape.py, shape_materialize.py and adopt-project.py: a value the YAML reader refuses for opening with an indicator is named by its line and says to double-quote it or reword it, plan --org @x names --org again under the reader's own remedy, and check names one bad tracking_branch once (#708) - #817
Conversation
… YAML reader refuses for opening with an indicator is named by its line and says to double-quote it or reword it, `plan --org @x` names `--org` again under the reader's own remedy, and `check` names one bad `tracking_branch` once (#708) Three refusals named the value to change on main or in v1.0.1, and a v1.1.0 change took that away (lane 4's L4-151, L4-152 and L4-127). L4-151, from #635 (#392, #393). The reader refuses a plain value that opens with an indicator, so an adoption plan whose hand-written `resolution:` opens with a backtick, which AGENTS.md's step 2 makes likely (`` `adopt-project.py` reads it ``), ended `check` in `REFUSED yaml-unparsable: <plan>: <words>`, exit 2, with no line among the hundreds a plan holds and no remedy, where it checked clean before. - `YamlError` carries a `line` and a `remediation`. `_scalar` and `_flow` read text and know no line, so the block reader records it: the new `_block_value` reads a block mapping's value and a block sequence's scalar item (`_parse_mapping`'s lone `-` refusal moves with it), and sets the line on a refusal raised there that carries a remedy. - `load_yaml` names it, `REFUSED yaml-unparsable: <path>: line N: <words>`, and passes the refusal's remedy on. The words of `parse_yaml` are unchanged: the tools that ask the reader about ONE rendered line quote them in a sentence of their own, where `line 1:` would be noise. - `_indicator_refusal` carries the remedy: write the value in double quotes, spelled by `double_quoted` (so the remedy's spelling reads back as the value), or reword it. That is #393's block-value openers and #392's flow-entry openers. The other refusals carry none (#370): the fix for an alias or an unclosed quote is no one edit. - Only a refusal with that remedy names its line. A line on every value's refusal (an alias, an unclosed flow, an empty flow entry) was measured: it changed words that the naming gate's `declared-unverified` reason and the root's `validate-pins.py` print whole, pinned by tests/test_pinned_copy_text.py, tests/test_naming_policy.py and tests/test_refusal_default_remediation.py, which is beyond this issue. - The project's own gates read their files with this copy: an indicator in a pin file is now the quoting remedy under `validate-pins.py`, where it printed the lockstep one, which is not a syntax error's remedy. L4-152, from #635. The reader refuses `repository: @x/Northwind`, so `plan --org @x` (a typo of `@opensoft`: GitHub owners never begin with `@`) stopped reaching the validator, whose `manifest-leg-repository` named `--org` (`FINDING_PLACEHOLDER`). It said "the manifest this run would write is a file the project's own YAML reader refuses", named no value, and its remedy said "the words after each value are the validator's own". - `_unread_sentences` renders each template line that carries a value of the caller's, by its own placeholder or by one `CARRIED_BY` names (each leg's `repository:` carries `ORG`, as the validator's finding about it does), reads it alone as `_misread_rows` reads one, and names the value where the reader refuses its line: `--org is '@x', which the project's own YAML reader refuses: <the reader's words for each line>`, main's sentence with the reader in the validator's place. The validator's own words cannot be had: it never reads a file the reader refuses. Where no such line is refused, the sentence is the reader's refusal of the file, as before. - `MANIFEST_UNREADABLE_REMEDIATION` is that case's remedy: choose another value for each one named, and where none is named, the text the reader's words quote holds it; the words are the reader's. `manifest_value_problems` gives the case the kind `unreadable`, and `refuse_manifest_values` (`plan`, the scaffold) and `check`'s `_manifest_findings` (and so `execute`) give it that remedy, under the code they gave it before, `-manifest-invalid`. `MANIFEST_INVALID_REMEDIATION` is not reworded, so `scripts/family.py`'s copy of it does not move, and `family.py init --org @x` stays refused. L4-127, from #644 (#455). The validator finds a NUL or a lone surrogate in `tracking_branch`, and `check` asks it, so `tracking_branch: "a\x00b"` was `unsafe-value`, then `plan-manifest-invalid` in the validator's words, which name no plan key. No such value is in `checked_value`'s alphabet, so `unsafe-value` names every one. `_validator_ignores` leaves the validator's `manifest-tracking-branch` out where `checked_value` refuses the branch, as `LEG_PATH_CODES` leaves the leg paths to `_leg_findings`, and `_manifest_findings` and `_validator_judged` ask it alike. It asks `checked_value` alone, which starts no process, as the validator's question must not (test_the_question_runs_no_process_at_all). The issue's other option, a `FINDING_PLACEHOLDER` row, was measured: it names the key and still prints two findings. Pinned copy. `scripts/repo_shape.py` is the one pinned shape copy this changes. No digest of it is recorded in this tree; each root takes it through `update-shape.py` as an `upstream-changed` row, with v1.1.0's other bumps. Tests: tests/test_reader_refusals_named.py (new). In tests/test_yaml_indicator_openers.py, two assertions that pinned an opener's refusal without its line expect `line N:` on its row; in tests/test_adopt_plan_yaml_quoting.py, one that pinned the validator's remedy under the reader's refusal of a whole manifest expects the reader's. Closes #708 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR restores actionable, value-specific diagnostics across YAML parsing and manifest validation: indicator-based YAML refusals now identify their line and provide a valid quoting remedy, unreadable generated manifests identify the relevant option under the reader's remediation, and unsafe tracking branches no longer produce duplicate findings, with broad regression coverage. Sequence diagram for actionable YAML refusal diagnosticssequenceDiagram
participant Tool
participant load_yaml
participant parse_yaml
participant block_value as _block_value
participant indicator as _indicator_refusal
Tool->>load_yaml: load_yaml(path)
load_yaml->>parse_yaml: parse_yaml(text)
parse_yaml->>block_value: _block_value(value, line)
block_value->>indicator: _indicator_refusal(value)
indicator-->>block_value: YamlError(remediation)
block_value->>block_value: record line on refusal
block_value-->>parse_yaml: raise YamlError
parse_yaml-->>load_yaml: YamlError
load_yaml-->>Tool: Refusal(yaml-unparsable, line, remediation)
Sequence diagram for manifest reader-specific validationsequenceDiagram
participant Command
participant manifest_value_problems
participant _invalid_problems
participant YAMLReader
participant refuse_manifest_values
Command->>manifest_value_problems: manifest_value_problems(values, ...)
manifest_value_problems->>_invalid_problems: _invalid_problems(...)
_invalid_problems->>YAMLReader: read rendered manifest lines
YAMLReader-->>_invalid_problems: YamlError for unreadable value
_invalid_problems-->>manifest_value_problems: unreadable sentence naming option
manifest_value_problems-->>refuse_manifest_values: kind and sentence
refuse_manifest_values-->>Command: -manifest-invalid with reader remediation
Flow diagram for deduplicating unsafe tracking branch findingsflowchart TD
A[check plan values] --> B[_validator_ignores]
B --> C{checked_value rejects tracking_branch?}
C -->|Yes| D[Ignore manifest-tracking-branch]
C -->|No| E[Keep validator findings]
D --> F[Report unsafe-value once]
E --> G[Report applicable validator findings]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
(lane openRepoShape-1, 2026-10-17T12:24:55Z) Lane 1's independent review of #817 at 0bc29c9: CHANGES NEEDED All three fixes work, and decision 7 holds when measured. One thing must change: the new quoting remedy gives the wrong instruction for a flow-mapping entry whose key opens with an indicator. If a person follows it exactly, they get a new refusal. The fix is small and I tested it. There are also two corrections to the PR body and one Sonar S9073. How I measured: the head is merged locally with main Decision 7, attacked first: holdsA root scaffolded from main
"No digest or blob of it is recorded in the tree" is true. At the head, Mixed versions are safe:
The issue's lines
Findings, ranked1. Must change: a flow-mapping entry whose key opens with an indicator gets a remedy that, followed exactly, is refused again.
Repro through the tool: append Do exactly that ( On the reader alone, the remedy's spelling is refused again for every mapping-key opener:
Flow-mapping values ( Fix, tested on the merge: -def _refuse_entry_indicator(text: str) -> None:
+def _refuse_entry_indicator(text: str, remedy: bool = True) -> None:
@@
- raise _indicator_refusal(text)
+ refusal = _indicator_refusal(text)
+ if not remedy:
+ refusal.remediation = ""
+ raise refusal
@@ def _split_key_flow
- _refuse_entry_indicator(text)
+ # A MAPPING ENTRY opens its KEY: quoting the whole entry, as the
+ # remedy would spell it, reads as an entry without a key (#708).
+ _refuse_entry_indicator(text, remedy=False)A remedy that quotes only the key ( @pytest.mark.parametrize("body", ["k: {@a: b}\n", "k: {`a`: b}\n", "k: {: a}\n",
"k: {? a: b}\n", "k: [x, {%a: 1}]\n"])
def test_a_flow_mapping_key_is_not_told_to_quote_the_whole_entry(body):
with pytest.raises(YamlError) as caught:
parse_yaml(body)
assert caught.value.remediation == ""2. PR body: the L4-127 "Main:" sentence has the order backwards. Measured on main for both values, 3. Sonar S9073 (a composite assert) in new test code, at 4. Low, possible S1192. The literal Checked and clean
Exact text replacements (PR body)
For the checklist (outside #708)
Lane: openreposhape-1 (openRepoShape-1) |
… is refused with no remedy and so no line, because the whole entry double-quoted reads as an entry without a key, and shape_materialize.py names its problem kinds and their remedies once (#708) Fix round 1, from lane 1's review of #817 at 0bc29c9 (issuecomment-6233847800). Finding 1. `_split_key_flow` called `_refuse_entry_indicator` on the whole entry, so `k: {@A: b}` was told to write `"@A: b"` in double quotes, which reads as a flow-mapping entry without a key and is refused again. Lane 1 measured it through `adopt-project.py check` on a plan holding `notes: {@who: Test Human}`, and on the reader for `{@A: b}`, ``{`a`: b}``, `{: a}`, `{? a: b}`, `{#a: 1}` and `[x, {%a: 1}]`. Lane 1's tested fix is taken as given: `_split_key_flow` calls `_refuse_entry_indicator(text, remedy=False)`, so such a refusal carries no remedy, and so no line. A flow-sequence item and a flow-mapping value keep both. Test: test_a_flow_mapping_key_is_not_told_to_quote_the_whole_entry, lane 1's five bodies and `k: {#a: 1}`; all six fail at 0bc29c9. Finding 3 (S9073). The new test file's one composite assert is two asserts. No other `assert ... and ...` or `... or ...` is in the new or changed test code. Finding 4 (a possible S1192). The problem kinds `misread`, `invalid` and `unreadable` are named once, as `KIND_MISREAD`, `KIND_INVALID` and `KIND_UNREADABLE`, and `adopt-project.py` compares against `KIND_UNREADABLE`. No printed word changes. SonarCloud's list at 0bc29c9 held one issue more, S3358 at `refuse_manifest_values` (a conditional expression nested in another, the remedy chosen by kind). `KIND_REMEDIATIONS` maps each kind to its remedy, and the refusal takes it from there. No printed word changes. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
(lane openRepoShape-1, 2026-10-17T12:46:31Z) Lane 1's re-verification of #817 at 8fd2fa6: PR-READY Nothing must change. All four findings are closed, decision 7 still holds on current main, and the three runs pass. I measured on a local merge of
Anything new: nothing that must change.
Lane: openreposhape-1 (openRepoShape-1) |
|
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 8 hours and 10 minutes by commenting |
|
main gained #747 (#681, `6626a3c5`) and #817 (#708, `64ec47c3`) since 40e678e. No conflict, and neither touches README.md, the handbook, AGENTS.md, docs/ or the README cap's ledger, so every handbook marker reads as before. README.md is 1474 lines, its cap; AGENTS.md 494, its cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and #817's entries, #642's upgrade note for a holder's `cd` line, and v1.0.0's `--into` limit kept open, current to main 64ec47c (#563) RV630's pre-review of the B2 stage (lane 3's session): - SHOULD-1: the CI bullet said every test that clones a just-pushed local remote clones it safely (#554), but five plain-path `git submodule update --init` runs are left on main (tests/test_doctor_legs_members_next.py:631, :987, :1054; tests/test_family_bump_remove_rollback.py:337, :908). It now says "runs `git clone` on", which is what #554 changed. - NIT-1: #812's READMEs quote a local clone URL "that needs it". - NIT-2: #682's first sentence no longer repeats the lead ("there:"). Landed since 40e678e, each entry checked against its squash: - #747 (#681), 6626a3c, whose tree equals `merge-tree 40e678e ff7c59b`: a new family bullet, with RV630's NIT ("`member-gitlink-absent`'s remedy puts the gitlink back by hand"); v1.0.0's limits at v1.0.0.md:466 and :467 are said closed. - #817 (#708), 64ec47c, whose tree equals `merge-tree 6626a3c d63a267`, and whose own lines equal those of 8fd2fa6, the head RV630 read: RV630's text, appended to the YAML reader bullet. #738 (#695) is v1.1.1 (lane 5, #473 issuecomment-6236564004), so its docs entry is not taken, and v1.0.0's `--into` limit (v1.0.0.md:492) is listed as still open under Known limits. Its upgrade note is true of main since #642 (0ed1b84), so it goes in now, under "Install and upgrade": lane 5's sentence verbatim, and the README's `Y/Y` layout. Counts and the open list at main 64ec47c: 113 pull requests since bfed143; 124 of 135 milestone issues closed at 16:42Z; 11 open, each with a pull request open, #681 and #708 out, #695 marked as planned for v1.1.1. All still placeholders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main bad05d2 brings #747 (#681), which changed `_check_shape_copy` in templates/assembly-root/scripts/validate-pins.py, and #817 (#708), which changed `repo_shape.load_yaml`'s `yaml-unparsable` clause and added tests/test_reader_refusals_named.py. One conflict, in `_check_shape_copy`: #747 replaced `target = root / str(row.get("path"))` with its `path:` check (a row with no string path is `shape-pin-row`), and this branch changed the `is_file()` line under it. Resolved as the pull request's body has said since round 0: #747's check and `target = root / path`, then this branch's comment, `named = _shown(path)` and `if not _is_file(root, target, "shape-copy-unreadable", named):`. RV827 found it identical to its own resolution. `load_yaml` merged cleanly: #817 changed the `yaml-unparsable` clause and this branch the `OSError` clause and the docstring. One line merged cleanly and is wrong together (RV827's M2): tests/test_reader_refusals_named.py:199, from #817, expected the root's `validate-pins.py` to name the pin in its `yaml-unparsable` line by `pin.resolve()`, the absolute path. This branch names every file the gate names relative to the root (#712), so the line expects `contracts/spec-pin.yaml`. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… or a leg checkout the gate cannot reach or read is refused by name, relative to the root, once and with its cause's advice, and every name and path it prints is spelled (#712) (#827) Issue #712 gathers lane 4's checklist lines L4-103, L4-104, L4-106 and L4-144, with the riders L4-105, L4-143 and L4-102, on the assembly root's lockstep gate. This takes all seven. L4-103. The leg pin, the shape pin, each shape copy, each neutral-product pin and the manifest were asked with `Path.is_file()`. Up to Python 3.13 it raised a refused `stat`, so a `contracts/` at mode 000 or r-- ended the run in a PermissionError traceback, exit 1; on 3.14 it answered False, and the run refused `pin-missing`, a present file said not to exist, under THE LOCKSTEP RULE's remedy. The five sites are `_is_file` now: #633's `_is_a`, and a refused `stat` is a refusal naming the file. A name no `stat` can be handed (a NUL in a role or a product) is no file, as `is_file()` answered. A shape copy at mode 000 passed its `stat` and ended in `file_sha256`'s PermissionError on every Python, in the same function; its open is the same refusal. A copy has no id for one, so it is `shape-copy-unreadable`, beside `shape-copy-missing` and `shape-copy-drift`; the YAML files keep `yaml-unreadable`, the id `load_yaml` gives them. Checkouts this user cannot search (RV827's S1, fix round 1). A leg's checkout at 000 or r-- (`_check_pin_digest`'s `(submodule / ".git").exists()`) and a checkout beside the root at 000 (`resolve_neutral_pin_source`'s) raised `PermissionError` out of the run on 3.11 to 3.13; from 3.14 the leg would have read as an uninitialized submodule and the sibling been passed over for `gh api`. Both ask `_is_there`, `_is_a` with any kind. The leg is `pin-submodule-unreadable`, beside `pin-submodule-uninitialized`, naming the `.git` it could not reach (`spec/.git: cannot be read (Permission denied)`) with the cause's advice; the sibling is there, so it is the source, and the pin's `neutral-pin-source-unreadable` finding says it could not answer. L4-104. A pin this user cannot open was `load_yaml`'s line, `<absolute path>: [Errno 13] Permission denied: '<absolute path>'`, with the lockstep remedy, which moves a pin and says nothing of access. `_load` reads the `OSError` the refusal is raised from, which `load_yaml` has chained since it was written, and names the file relative to the root, POSIX, once: `contracts/spec-pin.yaml: cannot be read (Permission denied)`, with a remedy of its own, so `main` adds no other. Read off the cause and not off new library words, so a root whose `scripts/repo_shape.py` is older than its `validate-pins.py` names the file the same way (#370, #610). `load_yaml` itself names the path once for every caller: `<path>: cannot be read (<the system's reason>)`. One rule for every file the gate names, on the coordinator's ruling: the other refusals `load_yaml` raises here (not UTF-8, not parsable) keep their id, words and remedy and name the file relative to the root, and so do `not a mapping` for the four YAML files, `shape-pin-missing`, `manifest-missing` and `manifest-no-legs`, which named the absolute path raw. L4-105. Every file this gate cannot open is told what its cause needs. A permission keeps the words it had (a workflow) or gets bump-leg.py's (#383) for a pin, the manifest or a copy: `chmod u+r` on a file, `u+rx` on each directory above it. Any other cause, an I/O error or a name too long, gets bump-leg.py's other sentence and is never told to `chmod`. `_Unreadable` carries the cause, so one workflow refusal naming both kinds says both. L4-106, L4-144 and every other print, on the coordinator's ruling (option A). A workflow's name, a pin's path built from a role or a `neutral_product_pins:` entry, a leg's path, role and repository, a pin's `source_repository`, a declared product, a shape row's path and digest, the shape pin's and the manifest's commits, a `--pin-source` value with no path and the root in a printed `bump-leg.py` line each reached stderr or stdout raw: a newline began a line, a bidi control reordered one, a C1 control broke one, an ESC was the control it is. Each is printed as `_shown` spells it now: `repr` where a character is not printable, the same bytes where every one is. A word of a printed command is `shlex.quote`d as before where it is printable, and `_shown`'s where it is not (`_quoted`): no quoting makes a control character pasteable, and quoted it still reached the terminal. The workflow-reference note's repository is not spelled: it prints only a repository equal to a reference `WORKFLOW_REF_RE` matched, which is printable ASCII. L4-143. A `--pin-source` under a `neutral_product_pins:` that is not a list was noted "project.yaml declares no neutral-product pin"; it says the field is not a list and points at the `manifest-neutral-product-pins` finding. L4-102. `PinSources` keys `--pin-source` products by `pin_product_key`, the pin file's own fold, as #638 keys the naming gate's `LinkSources`: `casefold()` took `openss=` for `openß`, two pin files. Re-pin: `templates/assembly-root/scripts/validate-pins.py` and `scripts/repo_shape.py` are pinned shape copies. As #634 says, no digest is recorded in this repository; each root and holder reads the copies it carries as `upstream-changed` in its `contracts/shape-pin.yaml` (both in an assembly root, `scripts/repo_shape.py` in a family holder), which `update-shape.py apply` re-copies and re-pins. Tests: tests/test_validate_pins_named_and_spelled.py (new): the issue's modes through the real gate, the five sites under a refused `stat` on every Python, every file named relative to the root, each cause's advice, one row for each print over a newline, a bidi control, a C1 control, an ESC and a byte that is not UTF-8 (and a printable control), the issue's own pin entry and workflow names, the L4-143 note and the L4-102 keys, a leg checkout and a checkout beside the root this user cannot search, and a role or a product spelled as an absolute path. Four existing tests held the old absolute path: tests/test_refusal_default_remediation.py (the `yaml-unparsable` line under the lockstep remedy), tests/test_bump_leg_unreadable.py (the validator names the other leg's pin POSIX now, on Windows too), tests/test_reader_refusals_named.py (#817's `yaml-unparsable` line from the root's gate, which names `contracts/spec-pin.yaml`) and a docstring in tests/test_validate_pins_unreadable_workflow.py. Closes #712 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st it would write, `family-manifest-invalid` names the value on the line it refuses, `--org is '@x'`, and gives the reader's remedy, not the validator's (#929) (#960) The family half of #459's U119 (U151), which `64ec47c` (#817, #708) left when it did the same for the scaffold and `adopt-project.py`. The reader refuses `repository: @x/<Family>`, so `init --org @x --local-remote-dir <dir>` (and `--dry-run`) said "the manifest this run would write is a file the holder's own YAML reader refuses: ...", named no value, and its remedy said "the words after each value are the validator's own", when the words were the reader's and the validator was never asked. - `shape_materialize._unread_sentences` takes the manifest's file name, the `CARRIED_BY` map and the words that name the reader, each the project's unless given, so its sentences and bytes for a project do not move. - `family.py`'s `_manifest_problems` gives each sentence a kind, as `_invalid_problems` does for a project: on the reader's refusal it asks `_unread_sentences` of `family.yaml`, whose `repository:` carries `--org` (`CARRIED_BY`), with "the holder's own YAML reader". `family_manifest_invalid` returns its sentences without their kinds. - `refuse_unholdable_manifest` refuses `family-manifest-invalid` still, with the remedy for the kind (`KIND_REMEDIATIONS`): the validator's for a finding, and the new `MANIFEST_UNREADABLE_REMEDIATION` for the reader's refusal. That opens with `UNREADABLE_CHOICE`, the first sentence of the project's reader remedy, now its own constant, and says the words are the holder's reader's. Measured before, at 64464d3 (family.py as at 5017830): `init --org @x --family Orbit --created-by 'Test Human' --dry-run --local-remote-dir <dir> --work-dir <dir>` exits 2 with the unnamed sentence and the validator's remedy. Now it says `--org is '@x', which the holder's own YAML reader refuses: a plain scalar cannot open with `@`, which is reserved in YAML: '@x/Orbit'.` under the reader's remedy, both runs, nothing created. Tests: tests/test_family_init_yaml_words.py, which also holds that the manifest that is not a mapping keeps the validator's kind and remedy. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #708 (comment)
What changed and why
Three refusals named the value to change on main or in v1.0.1, and a v1.1.0 change took that away (lane 4's L4-151, L4-152 and L4-127). Each case below was measured on main
bf1f099band on this head.L4-151 (from #635): a plan whose
resolution:opens with a backtick.adopt-project.py checkexits 2 withREFUSED yaml-unparsable: <plan>: a plain scalar cannot open with a backtick, which is reserved in YAML: 'adopt-project.pyreads it'. It gives no line and no remedy.check.YamlErrornow carrieslineandremediation._indicator_refusalcarries the remedy, with the value spelled bydouble_quoted, so the spelling it gives reads back as the value. It covers repo_shape.py:_scalarreturns a BLOCK plain scalar that opens with]or}as text, sok: ],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 readsk: ,,k: @a, `k:aandk: %a` #393's block-value openers, and repo_shape.py: a flow collection with an empty entry (k: [,a],k: [a,,b],k: {,}) or an entry that opens with:,#,-or?(k: [:],k: {:":},k: [#a],k: [- a]) is read, where PyYAML refuses, and a single pair in a flow sequence (k: [a: b]) reads as the string'a: b', where PyYAML reads[{'a': 'b'}]#392's flow-sequence items and flow-mapping values. A flow-mapping entry whose key opens with an indicator ({@a: b}) gets no remedy, so no line, because double-quoting the whole entry reads as an entry without a key._block_valuereads a block value or item. It records the line on a refusal that carries a remedy.load_yamlnames that line and passes the remedy on.parse_yamlare unchanged, because tools that ask the reader about one rendered line quote them in a sentence of their own.load_yamlrefusal (validate-pins.py,validate-manifest.py,validate-family.py,bootstrap.py) now names the line and gives the quoting remedy for such a value;validate-pins.pyprints it in place of the lockstep remedy, because a syntax error is not a stale pin.L4-152 (from #635):
plan --org @xnames--orgagain.REFUSED adopt-manifest-invalid: the manifest this run would write is a file the project's own YAML reader refuses: ... '@x/Northwind'.It names no value, and its remedy says "the words after each value are the validator's own".REFUSED adopt-manifest-invalid: --org is '@x', which the project's own YAML reader refuses: a plain scalar cannot open with@, which is reserved in YAML: '@x/Northwind'. ... '@x/Northwind-spec'. ... '@x/Northwind-code'.This is main's sentence with the reader in place of the validator; the validator never reads a file the reader refuses.MANIFEST_UNREADABLE_REMEDIATION: "choose another value for each one named; where none is named, the text the reader's words quote holds it. The words are the project's own YAML reader's, ...".checkandexecuteon a hand-editedorg: "@x", and forscaffold-project.py --org @x(with and without--dry-run), each under its code from before (-manifest-invalid)._unread_sentencesrenders each template line that carries a value, by its own placeholder or throughCARRIED_BY, where each leg'srepository:carriesORG, asFINDING_PLACEHOLDERmaps the validator's finding about it. It reads each line alone, as_misread_rowsdoes.MANIFEST_INVALID_REMEDIATIONis not reworded, soscripts/family.py's copy does not move.family.py init --org @xstays refused.L4-127 (from #644): one bad
tracking_branch, one finding.checkprintedFINDING plan-manifest-invalid: ... manifest-tracking-branch: ..., which names no plan key, and thenFINDING unsafe-value: tracking_branch is 'a\x00b', ....unsafe-valuealone._validator_ignoresleaves the validator'smanifest-tracking-branchout wherechecked_valuerefuses the branch, asLEG_PATH_CODESdoes for the leg paths. Every value that rule finds (a NUL, a surrogate) is outsidechecked_value's alphabet. It askschecked_valuealone, because the validator's question starts no process.FINDING_PLACEHOLDERrow, was measured: it names the key and still prints two findings.executealready saidunsafe-valuealone on main, and still does.Fix round 1 (lane 1's review at
0bc29c94, issuecomment-6233847800), in commit8fd2fa64on top of it:_split_key_flowcalls_refuse_entry_indicator(text, remedy=False), so a flow-mapping entry whose key opens with an indicator is refused with no remedy and no line. Before,k: {@a: b}was told to write"@a: b", which is refused again asflow mapping entry without a key.assert ... and ...or... or ...is in the new or changed test code.KIND_MISREAD,KIND_INVALIDandKIND_UNREADABLE. No printed word changes.0bc29c94held one issue more, S3358 atrefuse_manifest_values(the remedy chosen by a conditional nested in another).KIND_REMEDIATIONSmaps each kind to its remedy. No printed word changes.Pinned copy:
scripts/repo_shape.pyis the only pinned shape copy this changes. No digest of it is recorded in this tree; each root takes it asupstream-changedthroughupdate-shape.py, with v1.1.0's other bumps.Tests
tests/test_reader_refusals_named.pyis new: 44 tests on the harnesses oftests/test_adopt_manifest_values.py,tests/test_scaffold_manifest_values.pyandtests/test_family_init_values.py, imported from them.check: exit 2, and stderr equal in full to the line plus the remedy;check;load_yaml's detail and remedy for an opener in a block value, a sequence item, a nested value, a flow entry, a mapping in a sequence and a lone?;{@a: b},{`a`: b},{: a},{? a: b},[x, {%a: 1}]and{#a: 1}(lane 1's test, with its five bodies and the sixth it listed; all six fail at0bc29c94);validate-pins.pygives the line and the quoting remedy, not the lockstep one.plan --org @xand--org @opensoft: stderr in full, no plan written;checkandexecute(no leg created) on a hand-editedorg: "@x";scaffold-project.py --org @xwith and without--dry-run, nothing created;refuse_manifest_values;family.py init --org @xstill refused, with and without--dry-run.tracking_branch: "a\ud83db"and"a\x00b"give exactly one FINDING,unsafe-value;_validator_ignoresdrops the validator's finding only whereunsafe-valuefires.Existing tests changed:
tests/test_yaml_indicator_openers.py: two assertions that pinned an opener's refusal without its line now expectline N:on that row.tests/test_adopt_plan_yaml_quoting.py: one assertion that pinned the validator's remedy under the reader's refusal of a whole manifest (L4-152's defect) now expects the reader's.Fix-round run on
8fd2fa64's bytes (sha256 recorded at its start, equal to the commit's blobs), Linux, Python 3.12.3, pytest 9.1.1, noGIT_ALLOW_PROTOCOL: 1769 passed, exit 0, in 5:14. The 19 files:test_yaml_*,test_load_yaml_refusalsandtest_adopt_plan_yaml_quoting;test_pinned_copy_text,test_chain_remedy_callers,test_adopt_manifest_values,test_adopt_tracking_branch,test_naming_policyandtest_refusal_default_remediation;test_scaffold_leg_path_collisions, which guardsadopt-project.py's module attributes (it gains theKIND_UNREADABLEimport);test_repo_hygiene,test_cli_referenceandtest_agents_md_codes.First run, one targeted run on
0bc29c94's bytes (sha256 recorded at its start, equal after):test_yaml_*,test_load_yaml_refusals,test_refusal_default_remediation,test_template_text_placeholders,test_manifest_validator,test_scaffold_referent_chain_links,test_adopt_plan_escapes,test_doctor_validator_rows,test_doctor_validator_verdicts,test_doctor_pin_restore_merges,test_pinned_copy_text,test_naming_policy); every manifest-refusal and tracking-branch test (test_adopt_manifest_values,test_scaffold_manifest_values,test_family_init_values,test_adopt_plan_yaml_quoting,test_adopt_git_reads,test_adopt_origin_repository,test_adopt_branch_names,test_adopt_tracking_branch,test_bootstrap_branch_bytes);test_shape_doctorandtest_scaffold_leg_path_collisions, as every adopt change does; andtest_repo_hygiene,test_cli_referenceandtest_agents_md_codes.test_shape_doctor.py:10429; 16 + 3 intest_family_init_values.py:219and:291, where the template quotes the placeholder.test_shape_doctor.py:4154.An earlier local commit, never pushed, failed 5 of that set:
_validator_ignoresasked git, andtest_the_question_runs_no_process_at_allforbids any process there.test_pinned_copy_text.py. The same words are pinned intest_naming_policy.pyandtest_refusal_default_remediation.py.Both are fixed here, as "What it cannot tell" says.
Mutants, each reverting one part, run against the new file on
8fd2fa64's bytes (M5 re-targeted toKIND_REMEDIATIONS), with the sources restored and sha256-checked after each. All seven were killed:CARRIED_BYempty_validator_ignoresnever drops_split_key_flowasks with the remedyLane 1's test was also run against
0bc29c94's reader before the fix (6 failed) and after it (6 passed).What it cannot tell
declared-unverifiedreason and the root'svalidate-pins.pyprint whole, pinned intests/test_pinned_copy_text.py,tests/test_naming_policy.pyandtests/test_refusal_default_remediation.py. That is beyond this issue; it is named for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459's checklist below._unread_sentencesnames a value only where a line that carries it is refused when read alone. A refusal that only the whole file's context makes would still name no value. It then keeps main's sentence, under the reader's remedy. No such value was found for the shipped template.family.py's own reader branch (family_manifest_invalid) still says the validator's remedy under the holder reader's words. The issue scopesfamily.py init --org @xout ("its refusal stays"); this is named for Tracking: setup, scaffold, family and the shared helpers, creating projects and families #459's checklist.For #459's checklist, from lane 1's review (measured on this head merged with main
20004887), each outside #708:str.splitlines(), so a VT, FF,\x1c-\x1e, NEL, U+2028 or U+2029 inside a comment shiftsline Nby one ("# a\x0b# b\nk: `x`\n"saysline 3where an editor shows line 2); everyline N:refusal had this before this PR.@k: vreads as the key'@k'on main and here, while{@k: v}is refused (pre-existing).parse_yamlstill name no line:update-shape.py's two pin reads (:4992,:5035),shape-doctor.py:3760andfamily.py:2356.declared-unverifiedWARNING gainsline N:for an indicator refusal in a linked manifest, because its reason is built fromexc.detail; that is consistent, but no test covers it.check's remedy contradicts itself: its tail still says "re-runplanwith a value the validator accepts", under a remedy that says the validator cannot be asked._unread_sentencesdrops the whole-file refusal as soon as any line names a value, so a second, independent refused line would go unnamed; none was found in the shipped template.[@k: v]changes meaning when the remedy is followed: it spells"@k: v", which reads as a string where a pair may have been meant (a single pair in a flow sequence is refused anyway).repository:, the validator would name--org @xin main's exact words; that is a template change, outside this issue.Shared files
git merge-tree --write-treeof head8fd2fa64, each other head read byls-remotefirst:741679e755fd3f89fix/711-git-bash-dialect(repo_shape.py)8d074f16fix/758-git-command-scope(test_adopt_plan_yaml_quoting.py)b6e15591tests/test_adopt_empty_symlinks.py, which this PR does not touchfix/766-spelled-paths(shape_materialize.py)ef6db40dfix/689-next-push-refspec(shape_materialize.py)15c3b5abl3-785-non-ascii-paths(shape_materialize.py)4338345cfix/692-raw-folder-paths(shape_materialize.py)ab55c29fl3-563-docs(adopt-project.py)b2a62ec7GIT_CONFIG_COUNTentries the environment holds, so a pasted line keeps the session's maintenance settings and the Windows job's two (#758) #818's conflict is its own drift from main. It conflicts in the sametests/test_adopt_empty_symlinks.pyagainst this PR's basebf1f099balone, and intests/conftest.py,tests/test_adopt_empty_symlinks.pyandtests/test_no_auto_maintenance.pyagainst main741679e7.bf1f099bis 974d482's two lines ofadopt-project.py.make bootstrapend in a UnicodeDecodeError, and a character the filesystem encoding lacks still ends step (a) in a traceback #710, validate-pins.py: an unsearchablecontracts/still ends in a PermissionError, an unreadable pin is named by its absolute path twice, and a workflow name or a pin entry's control character reaches stderr raw #712 and repo_shape.py's YAML reader: Unicode spaces are stripped from text YAML keeps, the empty block key andk: {?: 1}still read, and three texts about the reader are one word out of date (what #635 left) #715 changescripts/repo_shape.pytoo (validate-pins.py: an unsearchablecontracts/still ends in a PermissionError, an unreadable pin is named by its absolute path twice, and a workflow name or a pin entry's control character reaches stderr raw #712 inload_yaml'sOSErrorbranch; this PR's hunk is itsexcept YamlErrorlines), and none is pushed yet. repo_shape.py's YAML reader: Unicode spaces are stripped from text YAML keeps, the empty block key andk: {?: 1}still read, and three texts about the reader are one word out of date (what #635 left) #715 lands after this one, by its issue.Closes #708
🤖 Generated with Claude Code
Summary by Sourcery
Improve refusal diagnostics so YAML errors identify actionable locations and remedies while manifest validation reports each invalid value only once.
New Features:
Bug Fixes:
Enhancements:
Tests: