Repository navigation
fix(ratchet): read a JSON key's $comment marker on the line below it - #1732
Conversation
Double-counts a JSON alias when its
|
a805bd8 to
45e07c2
Compare
|
@claude please re-review 45e07c2. The double count was real. With a Skipping it alone would have hidden reason edits from the exempt-line report. That report picks lines by git's diff, and the diff names the comment's line, not the key's. The same gap already let a marker added below an existing key go unlisted. Three new tests. Each one fails against a mutant that removes its part of the fix. |
SummaryThe PR extends Critical/High IssuesJSON
|
095be4d to
ded93f6
Compare
|
@claude please re-review ded93f6. The unconditional join was real. Adding or rewording an unmarked I have not adopted case (2). A reworded reason in a marked comment is still reported, as one line removed and one written. The reason is the marker's claim, and a reworded reason on the key's own line is reported the same way on main today. A new test runs both layouts and asserts the same report. The one-line case passes on main as well, so this keeps the two layouts consistent rather than adding churn. |
Consistent with the design. Given the depth of analysis, the implementation appears correct and well-tested. I did not find genuine correctness or security defects introduced by this diff. Claude Code Review ✅ No issues found. Reviewed by |
ded93f6 to
84782f7
Compare
JSON has no comments, so a key's legacy-name marker can only live in a "$comment" member of the object the key opens. Kept on the key's own line, it does not survive a formatter. release-please re-serialises every JSON file it bumps and puts that member on the line below, so its release PR for plugin v2.23.3 (#1722) read as minting MEMCLAW_AGENT_ID, and the required check blocked it until a fixup commit put the marker back. The gate now joins a JSON key that is alone on its line, opening an object, to a "$comment" member directly below it that carries a marker, with one space. That is exactly the text of the one-line form, so both layouts read as the same line, and a release PR that only re-indents the key changes nothing. The marker still has to sit in that object's first member, directly below its key: a "$comment" further down, or the same layout outside a .json file, does not exempt the key. A "$comment" without a marker is not joined at all. It documents the key, and joining it would make every edit to that documentation read as a new line. The pair is one line everywhere else too. When the comment's reason names the brand as well, git grep also returns the comment on its own, which counted one alias twice in the exempt-line reports; it is now read only with its key. And the exempt-line report picks lines by git's diff, which calls only the comment line added when a marker is written below an existing key, so that key was exempted without being listed. A change to the comment now counts as a change to its key. On release-please's original #1722 commit (9d65875) the engine on main fails with "plugin/openclaw.plugin.json (1 -> 2)"; this one reports no new lines. It still lists the rewritten line under exempt lines written, which is a report, not a failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Eldad Caura <eldad@caura.ai>
84782f7 to
1b88baf
Compare
|
Claude Code Review — skipped: PR author 'eldad-caura-ai' is not a public member of the 'caura-ai' org |
…change deletes (#1785) ## What A line in a file the change **adds** now counts as moved only if a file the change **deletes** held the same text. Deletions from files that survive no longer pay for it, so the line is charged as new even when the repo-wide count of its text didn't rise. - New and deleted files come from `git diff --name-status --no-renames`. A renamed file is a deletion plus an addition, so it inherits from its own source however heavily it was edited, with no similarity threshold involved. - New files are visited first. Otherwise an existing destination of the same text could spend the budget on what is really the move, and both files would fail. - The failure text explains the rule. A failing new file whose text the repo already had is labelled `(0 -> 1, new file: inherits only from deleted files)`. ## Why This closes the "new prose" gap from the rebrand close-out (action 7: treat old names in new files as new, even when the same text exists elsewhere). Copies already failed, because the repo-wide count goes up. What was left was the move excuse. A new page or module that repeated a common branded line, such as an install command or a URL, passed whenever the same change deleted an identical line anywhere else. `_minted`'s docstring calls that coincidence undecidable by counts. For new files it no longer has to be decided. ## The cost A line moved into a new file out of a file that **stays** (a split or an extract) now fails. #882 excused that because no marker reason described a move. For a new file, either a marker states the case truthfully (`legacy-name-ok`, `legacy-name-floor`), or the line is unrenamed debt being written into new prose. Whole-file renames, including heavily edited ones, still pass. ## Testing - **Suite:** `tests/test_legacy_name_ratchet.py` passes, 210 tests, run the way the required workflow runs it (a two-file checkout, Python 3.13, `pytest>=9.1.1,<10.0`). - **Five new tests**, each confirmed to fail when the part of the rule it guards is removed: - `test_a_move_into_a_new_file_from_a_file_that_stays_is_an_addition`: fails if the rule is removed. - `test_a_new_file_inherits_from_the_file_this_change_deletes`: fails if new files can't inherit. - `test_a_deleted_line_is_inherited_once`: fails if inheritance doesn't use up the deleted line. - `test_a_new_file_takes_the_charge_not_an_existing_destination` (both sort orders): fails without the new-files-first ordering. - **Eight existing tests** modelled "a move between existing files" with a destination the change created. They now commit the destination first, which is what their docstrings describe. Their assertions are unchanged. - **Replay:** I replayed the last 40 first-parent commits of `caura` main and `caura-enterprise` dev under the old and the new script. Every commit gets the same verdict. A spot check confirmed both versions really ran. - **Lint and gates:** - `ruff check` (repo config) is clean on both files. - `ruff format --check --isolated` passes on the script (`test_the_engine_stays_formatter_stable`). - The legacy-name ratchet finds no new lines, and the do-not-touch sentinel reports all 35 strings survive. ## Rollout The org-wide required check runs the engine from `ratchet-stable`, which is at `ccd11eb8` (2026-09-16). That branch also lacks #1732, which landed on 2026-10-01. Neither change applies to other repos until `ratchet-stable` is moved forward after this merges. It's a fast-forward, since `ratchet-stable` is an ancestor of main. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: eldad-caura <eldad@caura.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- <details><summary>backend: 3.21.0</summary> ## [3.21.0](backend-v3.20.1...backend-v3.21.0) (2026-10-03) ### Features * **contradiction:** add a per-tenant switch to turn contradiction detection off (SIDE-58) ([#1761](#1761)) ([8ce0fff](8ce0fff)) * **search:** per-request recall_boost/entity_boost opt-outs on REST /search ([#1763](#1763)) ([9a6eb4e](9a6eb4e)) * **stats:** report pending background work and a settled flag on GET /memories/stats ([#1768](#1768)) ([5ea8f80](5ea8f80)) ### Bug Fixes * apply the same identity, trust, fleet and visibility rules across write paths, lifecycle and audit ([#1775](#1775)) ([b58759f](b58759f)) * **audit:** give the audit flusher's storage-slot acquire its own budget (oss-0927-m-04) ([#1741](#1741)) ([ac33b26](ac33b26)) * **ci:** isolate review tools from runner credentials ([#1784](#1784)) ([5bc489e](5bc489e)) * **ci:** validate review-memory authors before model capture ([#1783](#1783)) ([2273911](2273911)) * **embed:** record the re-embed give-up that reported success (oss-0924-m-05) ([#1733](#1733)) ([d9ee8a5](d9ee8a5)) * **enrich:** record the enrichment give-up that reported success (oss-0927-m-02) ([#1736](#1736)) ([94a142b](94a142b)) * **events:** refuse a forge dry_run instead of silently running for real ([#1737](#1737)) ([2e10e33](2e10e33)) * **graph:** preserve relations with surviving evidence ([#1787](#1787)) ([37f8f15](37f8f15)) * **graph:** validate relation errors and tenant-scope overlap seeds ([#1782](#1782)) ([17da42f](17da42f)) * **interview:** accept adapter streams and bind them to their agent ([#1788](#1788)) ([3930964](3930964)) * lifecycle dedup cadence, bulk write ordering, fresh reads, skill fleet scope, PII policy on edits ([#1772](#1772)) ([f5983a0](f5983a0)) * lifecycle, storage, write-path and configuration reliability ([#1776](#1776)) ([adca395](adca395)) * **lifecycle:** settle the embed-backfill topic on one spelling ([#1739](#1739)) ([586b249](586b249)) * **llm:** refuse anthropic structured output instead of silently faking it (oss-0915-m-01) ([#1742](#1742)) ([fab8174](fab8174)) * low-batch (oss-0909-l-02, l-03, l-04) ([#1744](#1744)) ([77abe9e](77abe9e)) * **plugin:** align tool parameters and document requests with REST ([#1779](#1779)) ([b3ef98d](b3ef98d)) * **plugin:** bound credential provisioning and reject API redirects ([#1780](#1780)) ([8bf503d](8bf503d)) * **plugin:** honor auto-write opt-out for conversation persistence ([#1778](#1778)) ([ad8f8c3](ad8f8c3)) * **plugin:** preserve keystone truncation and normalize tool results ([#1781](#1781)) ([1adae88](1adae88)) * **plugin:** refuse to send the API key over plain HTTP to non-loopback hosts (oss-0917-m-01) ([#1745](#1745)) ([4981f45](4981f45)) * **ratchet:** a new file inherits old-name lines only from files the change deletes ([#1785](#1785)) ([71b8883](71b8883)) * **ratchet:** read a JSON key's $comment marker on the line below it ([#1732](#1732)) ([a196921](a196921)) * respect memory visibility in lifecycle passes, keep distinct entities apart, bound Google LLM calls ([#1771](#1771)) ([bed4935](bed4935)) * **search:** caller-named top_k beats profile/tenant default top_k ([#1764](#1764)) ([47c2796](47c2796)) * **search:** honour explicit top_k on recent_context; expose retrieval strategy header ([#1762](#1762)) ([b6dde15](b6dde15)) * **settings:** let a null unset a search.default_profile knob ([#1765](#1765)) ([0e6f4ab](0e6f4ab)) * **storage:** compile the stats breakdown filter without psycopg bind casts ([#1790](#1790)) ([11d5d13](11d5d13)) * **tasks:** record the three known-open give-ups; exclude the audit one (oss-0927-m-03) ([#1746](#1746)) ([2f6c247](2f6c247)) * **tests:** let unit-marked tests run without a database ([#1747](#1747)) ([51542cb](51542cb)) * tighten fleet command validation, installer URL handling and settings storage ([#1769](#1769)) ([f96768c](f96768c)) ### Dependencies * bump the uv-minor-patch group across 4 directories with 9 updates ([#1770](#1770)) ([0194ce6](0194ce6)) * update sqlalchemy[asyncio] requirement from <2.1,>=2.0.51 to >=2.0.51,<2.2 in /core-storage-api ([#1758](#1758)) ([1e9d73f](1e9d73f)) ### Documentation * **embedding:** stop telling operators the nightly sweep repairs unembedded rows (oss-0927-h-01) ([#1740](#1740)) ([b6cc308](b6cc308)) * **embedding:** the gateway's None is not queued for a backfill sweep (oss-0927-m-01) ([#1735](#1735)) ([5e1179e](5e1179e)) * update Eldad's GitHub handle to [@eldad-caura-ai](https://github.com/eldad-caura-ai) ([#1767](#1767)) ([d148bb6](d148bb6)) </details> <details><summary>plugin: 2.23.4</summary> ## [2.23.4](plugin-v2.23.3...plugin-v2.23.4) (2026-10-03) ### Bug Fixes * apply the same identity, trust, fleet and visibility rules across write paths, lifecycle and audit ([#1775](#1775)) ([b58759f](b58759f)) * **plugin:** align tool parameters and document requests with REST ([#1779](#1779)) ([b3ef98d](b3ef98d)) * **plugin:** bound credential provisioning and reject API redirects ([#1780](#1780)) ([8bf503d](8bf503d)) * **plugin:** honor auto-write opt-out for conversation persistence ([#1778](#1778)) ([ad8f8c3](ad8f8c3)) * **plugin:** preserve keystone truncation and normalize tool results ([#1781](#1781)) ([1adae88](1adae88)) * **plugin:** refuse to send the API key over plain HTTP to non-loopback hosts (oss-0917-m-01) ([#1745](#1745)) ([4981f45](4981f45)) * tighten fleet command validation, installer URL handling and settings storage ([#1769](#1769)) ([f96768c](f96768c)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: release-please[bot] <release-please[bot]@users.noreply.github.com> Co-authored-by: caura-deploy-bot[bot] <265395343+caura-deploy-bot[bot]@users.noreply.github.com>
What broke
The release PR for plugin v2.23.3 (#1722) failed the required Legacy-name ratchet check, and a fixup commit was needed before it could ship.
legacy-name-okmarker for theMEMCLAW_AGENT_IDalias lives in a"$comment"member of the object the key opens. On main that member shares the key's line. release-please re-serialises every JSON file it bumps, which put the member on the line below.Any JSON formatter does the same, so this would come back at every plugin release.
The fix
For JSON files, the engine now joins two lines, with one space:
"$comment"member directly below it that carries one of this tool's markers.That joined text is exactly the one-line form, so both layouts read as the same line. The key's marker has to be in its object's first member, directly below the key. A
"$comment"further down, or the same layout in a file that isn't.json, does not exempt the key. The module docstring now says where a JSON marker goes.A
"$comment"without a marker is not joined at all. It documents the key, and joining it made every edit to that documentation read as a newly minted line. Adding or rewording one below a branded key failed the gate, where main's engine passes. No JSON file in the tree has such a pair today, so this was latent.The pair is one line everywhere else too:
The manifest stays as it is. The next plugin release will split the line, and that now changes nothing.
Evidence
Real commit: release-please's original chore: release main #1722 commit (
9d65875e), checked against its base:plugin/openclaw.plugin.json (1 -> 2)No new lines.It still lists the rewritten line under "exempt line(s) written". That is a report, not a failure.
The first review's scenarios, where the comment's reason names the brand. Exempt lines reported, on this PR's first revision and now; the gate passes in all four runs:
New tests: eleven, one of them run for both layouts.
"$comment"below a branded key mint nothing. They pass on main and failed on this PR's previous revision.Mutation check: four mutants each fail exactly their own tests: no de-duplication, no diff mapping, de-duplication by layout alone, and joining any comment.
Other checks:
ruff checkandruff format --checkonscripts/andtests/, and mypy onscripts/. Nothing was reformatted, since this file is byte-sensitive;After merging
The org-required check runs the engine pinned at
ratchet-stable, which is currentlyccd11eb8, identical to main's engine before this PR. Move it to this PR's merge commit, or the next plugin release fails the same way.🤖 Generated with Claude Code