feat(workspace): preserve comments in workspace yaml edits via yamlpatch#511
feat(workspace): preserve comments in workspace yaml edits via yamlpatch#511
Conversation
Route every workspace yaml writer (allowBuilds, patchedDependencies, catalog upsert/cleanup, config TUI key writes) through `yamlpatch` instead of round-tripping through `yaml_serde::to_string`. yamlpatch edits the original source surgically — keys, comments, and whitespace the closure didn't touch land back on disk byte-identical, so the daily install (cleanupUnusedCatalogs) and one-shot edits (`approve-builds`, `patch-commit`, `patch-remove`, `config set --location workspace`) no longer wipe user annotations. `edit_workspace_yaml` keeps its closure-based public API. Internally it diffs the parsed before/after `yaml_serde::Mapping`, reduces the diff to a minimal sequence of `Add` / `Replace` / `Remove` patches, and applies them via `yamlpatch::apply_yaml_patches`. The empty-file path still falls through to `yaml_serde` since there are no comments to preserve. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`aube remove` previously rebuilt every dep section from `BTreeMap` via `sync_manifest_dep_sections`, which alphabetized the surviving keys whenever the user dropped one — a one-line uninstall would reshuffle the entire manifest as a side effect. Pnpm and npm leave the on-disk order alone and only delete the named entries, and that is what `aube remove` should do too. Mutate the parsed `serde_json::Map` in place inside `update_manifest_json_object` instead. Use `shift_remove` instead of the default `remove`, since serde_json's `Map` is an `IndexMap` under the `preserve_order` feature and the default is `swap_remove`, which would still scramble surviving keys. Same fix applied to `prune_sidecar_entries_json` so namespaced and top-level cleanups also keep their order. `aube add` continues to go through `sync_manifest_dep_sections` — inserts are expected to land in a stable sorted spot, so the BTreeMap path is the right call there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile Summary
Confidence Score: 4/5Safe to merge; no P0/P1 issues found, and previous review concerns have been addressed. No critical or likely-bug findings. The one P2 (silent The Important Files Changed
Reviews (5): Last reviewed commit: "fix(workspace): harden yaml diff against..." | Re-trigger Greptile |
CI surfaced two yamlpatch issues the first version didn't catch:
1. `Op::Add { value: Mapping(...) }` for non-empty nested mappings
strips the inner indentation hierarchy and emits invalid YAML
(every nested line lands at the parent's column). Trips on
first-time `catalogs:` upserts and any new `allowBuilds:` block.
2. `Op::Add { key }` interpolates the key verbatim, so a YAML-special
key like `@pnpm.e2e/...` lands unquoted and breaks the document
on the next parse — repros from the install-time auto-deny seed
when the existing entry uses a quoted key.
Route every Add (mapping or scalar) through a small block-style
emitter that resolves the parent's byte span via `yamlpath`, infers
the child indent from `extract_with_leading_whitespace`, and
inserts a properly-quoted, properly-indented entry into the source
text. yamlpatch is still used for `Remove` and `Replace` — those
work correctly and are where the comment-preservation win lives.
Other fallout this commit cleans up:
- Drop `yamlpatch` / `yamlpath` / `serde_yaml` from `aube/Cargo.toml`.
They were transitively pulled via `aube-manifest`; cargo-machete
failed CI because nothing in the `aube` crate referenced them
directly.
- Address Greptile P1: `to_serde_value` no longer `.expect()`s on
fallible serialization; errors propagate as `crate::Error`.
- Address Greptile P2: non-string mapping keys now error out instead
of being silently dropped from the diff.
New regression tests cover all three failure modes:
`edit_workspace_yaml_adds_nested_mapping_and_round_trips`,
`edit_workspace_yaml_adds_nested_mapping_under_existing_parent`,
`add_to_allow_builds_merges_with_quoted_existing_key`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eful Two follow-ups from the review bots on the previous commit: 1. `scalar_key_str` had a hand-rolled "safe character" set that omitted `@`, so non-leading-`@` keys like `b@2.0.0` and `is-positive@3.1.0` got unnecessarily double-quoted on the wire — contradicted the function's own doc string and produced style inconsistencies with the existing entries on disk. Defer to serde_yaml's emitter so plain-scalar quoting matches the rest of the file: `b@2.0.0` stays unquoted, `@scope/pkg@1.0.0` (leading reserved indicator) gets the canonical quoted form. 2. `clear_workspace_value` in the config TUI used to no-op silently on a workspace yaml whose top level wasn't a mapping (`Ok(false)`). Routing through `edit_workspace_yaml` turned that into a hard error. Restore the graceful path with a pre-flight check — clearing one key is a poor place to surface a parse error. New tests pin both behaviors: `upsert_workspace_patched_dependency_does_not_quote_unreserved_at_keys`, `upsert_workspace_patched_dependency_quotes_leading_at_keys`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cursor flagged the previous revision: silently mapping a yaml parse error to `Ok(false)` masks genuine file corruption from the user. A malformed workspace yaml is a real failure — surface it. Empty, missing, and non-mapping top-level shapes stay graceful (matches the pre-migration behavior of this command). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 425b039. Configure here.
Three review-flagged latent issues in `yaml_patch::apply_diff`: - `render_entry`'s catch-all arm fed sequence values through `render_scalar`, producing structurally invalid YAML when a sequence- valued key was added (`key: - a\n- b\n`). Add an explicit Sequence arm that emits block-style items at child indent, with a delegated path for the rare nested-mapping/sequence-in-list case. - `Op::Replace` of a scalar with a non-empty mapping went through yamlpatch's same broken serializer as `Op::Add` and would land children at the parent's column. Detect the type-change and split into a Remove + manual injection so `inject_entry` re-emits children at the canonical indent. - `detect_child_indent` fell back to a hard-coded column 2 for empty parents at depth >= 2, making new children siblings rather than descendants. Pass the parent's depth and use parent_depth * INDENT_STEP instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed review feedback in 2445eb2: Greptile P2 / Cursor — Greptile P2 — Cursor — Cursor — Cursor —
This comment was generated by Claude. |

Summary
yamlpatchinstead ofyaml_serde::to_string. Keys, comments, and whitespace the closure didn't touch land back on disk byte-identical, so the daily install (cleanupUnusedCatalogs) and one-shot edits (approve-builds,patch-commit,patch-remove,config set --location workspace) no longer wipe user annotations.edit_workspace_yamlkeeps its closure-based public API. Internally it diffs the parsed before/afteryaml_serde::Mapping, reduces the diff to a minimal sequence ofAdd/Replace/Removepatches, and applies them viayamlpatch::apply_yaml_patches. Empty/missing files still go throughyaml_serdesince there are no comments to preserve.aube removenow leaves the surviving dep entries in their original on-disk order. Previouslysync_manifest_dep_sectionsalphabetized the manifest as a side effect of dropping one name. Switched to in-placeshift_removeagainst the parsed JSON object, mirroring pnpm/npm. (aube addis unaffected — sorted inserts there are intentional.)Test plan
cargo clippy --all-targets -- -D warningscleancargo fmt --checkcleancargo test --workspacepasses (1442 tests across all crates)edit_workspace_yaml_preserves_comments_around_unchanged_keysupsert_workspace_patched_dependency_preserves_comments_on_real_changeremove_workspace_patched_dependency_preserves_comments_on_real_removeprune_preserves_comments_when_dropping_one_entryremove_preserves_dep_order_in_sectionaube approve-builds esbuildagainst a workspace yaml with# ...annotations on adjacent keys; confirm annotations survive.Notes
yamlpatch1.24,yamlpath1.24 (transitive:tree-sitter,tree-sitter-yaml),serde_yaml0.9 (yamlpatch's value type — yaml_serde stays for typed reads).Removeop, which only deletes thekey: valueline. Documented in thecleanupUnusedCatalogssetting docs as "clean up by hand if you don't want orphaned annotations" — the alternative (heuristic comment ownership) would risk eating real ones.🤖 Generated with Claude Code
Note
Medium Risk
Touches core workspace-YAML write paths and introduces a new diff/patch mechanism plus dependencies, which could cause subtle YAML output/edge-case regressions despite added tests.
Overview
Workspace YAML mutations are now comment/format preserving by routing writes through
yamlpatch/yamlpathinstead of round-tripping viayaml_serde, withedit_workspace_yamldiffing the before/afteryaml_serde::Mappingand applying minimalRemove/Replaceops plus custom injection for nested mapping adds.Workspace-level writers (e.g.
allowBuilds,patchedDependencies, catalog cleanup, and config TUI workspace writes) are updated to use this shared helper, and docs/settings are adjusted to reflect preserved comments (with note about possible orphaned annotations on removed entries).Separately,
aube removenow deletes dependencies and sidecar metadata in-place usingshift_removeto avoid reordering remainingpackage.jsonkeys, with new regression tests covering comment preservation and key-order stability.Reviewed by Cursor Bugbot for commit 2445eb2. Bugbot is set up for automated code reviews on this repo. Configure here.