Skip to content

Make every --json top-level error a {code, message} object (#704) - #1027

Open
Mikola Lysenko (mikolalysenko) wants to merge 13 commits into
mainfrom
arch-refactor/704-json-error-object
Open

Mikola Lysenko (mikolalysenko) wants to merge 13 commits into
mainfrom
arch-refactor/704-json-error-object

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

On 2026-10-07 the maintainer chose option 1 on #704 ("Option 1 is clearly the correct choice. Implement proposed solution."). Every --json failure now writes the top-level error as a {code, message} object, the same shape as EnvelopeError. This is a breaking change for v5.0. It also covers register row C53: usage errors (exit 2) follow rule (b), so under --json they print a coded error on stdout.

Changes

  • Shared helpers in src/json_envelope.rs: set_error, set_error_keep_status, legacy_error / print_legacy_error, and usage_error(cmd, json, code, msg) -> i32.
    • Under --json, usage_error prints a full envelope for apply, list, remove, repair, vendor and vex, and {status, error: {code, message}} for scan, get and rollback.
    • Without --json, it prints Error: … on stderr as before.
  • Guard tests: they fail on a bare return 2; under src/commands (hosted_bundle.rs is exempt), and on a "status": "error" JSON literal whose error is a string or has an errorCode next to it.
  • get: report_error takes a code. The lock failure, the nested-apply error, blob_write_failed and selection_required all use the object form, and the top-level errorCode is gone.
    • New codes: patch_no_applicable_files, offline_unsupported and identifier_invalid.
    • The nested-apply error keeps status: "partial_failure".
  • scan: there is now one hosted error emitter, and every caller passes a code. The discovery failure, --offline, api_batch_failed, the embedded VEX failure, the vendor step and policy_error_json all use the object form.
    • All nine exit-2 sites go through usage_error, with these codes: invalid_args, global_scope_unsupported, path_not_directory, path_glob_no_match, path_glob_invalid, path_outside_repo and invalid_env.
    • New codes: reference_resolve_failed and lockfile_write_failed.
  • rollback: emit_rollback_error(json, code, msg) is now used for every failure, and the inline objects use manifest_not_found, patch_not_found and rollback_failed. A bad glob is a usage error (path_glob_invalid). When a path pattern matches no patched packages, rollback reuses scan's path_glob_no_match.
  • remove, repair, vendor, vex: their exit-2 sites go through usage_error and keep their existing codes.

What users see

  • Scripts that read .error as a string or read the top-level .errorCode must now read .error.message and .error.code.
  • Usage errors from scan, remove and rollback under --json now print a coded JSON error on stdout, not just text on stderr. Under --json, the message goes only to stdout.
  • --global --mode vendored gives error.code: "global_scope_unsupported" on scan, get and vendor.
  • Clap's parse errors are unchanged and still print nothing on stdout.

Docs

  • CLI_CONTRACT.md:
    • It states the single error shape and updates the socket.yml error output and the rollback error key.
    • Every lock_held/errorCode mention is updated.
    • The new codes are added to the code table, and the exit-code 2 row states the stdout rule.
    • It adds a jq recipe for .error.code.
  • docs/migrating-to-v5.md has a new "JSON output" section.
  • CHANGELOG.md is not touched.

Tests

  • New: tests/json_error_shape.rs covers usage errors under --json for scan, get, rollback, remove, repair and vex. It also covers stdout staying empty without --json, the offline refusals, and rollback's manifest_not_found / patch_not_found.
  • Updated: about 70 assertions across 25 test files, plus scripts/backtest-pipenv.py, which compared error against a string.
  • Ran, all passing:
    • cargo test -p socket-patch-cli lib tests (868).
    • All cli_parse_* suites, plus cli, get, scan and rollback.
    • The four get/rollback/scan_hosted/scan_mod covgap suites.
    • e2e_socket_yml_policy, global_scope_project_state, scan_api_retry_e2e, in_process_rollback_hosted, in_process_redirect(_pnpm), e2e_vex, vex_terminal_output and json_error_shape.
    • cargo clippy adds no new warnings.
  • Not run locally: the docker suites and the real package-manager tests (e2e_redirect_vlt_build, mode_migration_vlt, the scripts/ backtests). Their assertions were rewritten the same way; CI will run them.

Review

The review found no correctness bugs. It fixed one wording error in the contract's nested-apply note and stale errorCode comments in the covgap get, scan_hosted and rollback tests.

These are left as is because they predate this branch:

  • rollback's lock failure prints a full Envelope (command: "rollback"), not the {status, error} shape. Its error is already {code, message}.
  • In scan agent mode, a manifest-read or lock failure can print the nested get error document before scan's own, so stdout gets two JSON documents.
  • run_scan picks global_scope_unsupported by matching the error message from resolve_mode_flags. This is fragile. Sibling Decide: warn on and then remove scan --apply/--vendor, and whether --vex stays embedded #966 rewrites the same spot, so expect a conflict in scan/mod.rs.

Closes #704

🤖 Generated with Claude Code


Note

Medium Risk
Breaking JSON contract for all automation that read string error or top-level errorCode; behavior is intentional v5.0 and heavily tested, but downstream scripts must migrate.

Overview
v5.0 breaking change: every --json failure now uses a single top-level error: {code, message} object (the EnvelopeError shape). The old string error and sibling errorCode are removed.

Shared helpers in json_envelope.rs (legacy_error, set_error, usage_error, etc.) centralize emission so get, scan (hosted/policy/vendor), rollback, remove, repair, vendor, and vex cannot drift. Usage errors (exit 2) under --json now print a coded object on stdout for legacy commands (scan, get, rollback) as well as envelope commands. Nested apply failures keep partial_failure while setting error.code. get adds stable codes (patch_no_applicable_files, offline_unsupported, identifier_invalid, …) and stops echoing forced-identifier typos in messages (CodeQL).

CLI_CONTRACT.md documents the unified shape, rollback's error key, expanded code tables, and jq recipes. Tests and backtest scripts were updated (~70 assertions); new tests/json_error_shape.rs guards the contract.

Reviewed by Cursor Bugbot for commit 9a15a0b. Configure here.


Generated by Claude Code

Comment thread crates/socket-patch-cli/src/json_envelope.rs Fixed
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] CI status: all checks green on 70c3229 (555 pass, 6 skipped).

Fixed: CodeQL rust/cleartext-logging (the forced --id/--cve/--ghsa shape error no longer echoes the argument); remove/scan tests now assert the coded usage error on stdout under --json; json_error_shape.rs spawns via hermetic::binary_command (spawn_env_hygiene). These failed on Linux too, not only Windows. Rebased onto origin/main (CLI_CONTRACT.md rollback table conflict: kept both the new error row and main's warnings wording). macOS/sbt/mill/coverage-merge failures were runner/apt infra and passed on rerun.


Generated by Claude Code

set_error / set_error_keep_status write the top-level error as a
{code, message} object and drop any top-level errorCode. legacy_error and
print_legacy_error give scan, get and rollback the minimal
{status: "error", error: {code, message}} shape. usage_error prints a
self-enforced usage error under --json (a full envelope for envelope
commands, the legacy shape for scan/get/rollback), or Error: on stderr,
and returns 2.

Guard tests fail on a bare `return 2;` in src/commands (outside the
hidden hosted_bundle harness) and on a "status": "error" JSON literal
whose top-level error is a string or carries errorCode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
)

get: report_error takes a code (patch_fetch_failed, manifest_unreadable,
manifest_write_failed, patch_no_applicable_files, offline_unsupported);
the lock failure, the nested-apply run error, blob_write_failed and
selection_required carry error: {code, message}; top-level errorCode is
gone. The nested-apply error keeps status partial_failure.

scan: one hosted emitter that requires a code (refusals, lock_held/
lock_io, patch_details_failed, reference_resolve_failed,
lockfile_write_failed); discovery failure, --offline, all-batches-failed
(api_batch_failed), embedded VEX failure, the vendor step and the
socket.yml refusal all write the object.

rollback: emit_rollback_error takes a code; manifest_not_found,
manifest_invalid, manifest_unreadable, patch_not_found,
path_glob_no_match, hosted_wiring_contested, vendor_ledger_missing and
rollback_failed.

Usage errors (exit 2) in scan, get, rollback, remove, repair, vendor and
vex go through json_envelope::usage_error, so under --json they print the
coded error on stdout. Breaking change to the v5.0 JSON contract.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…704)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…704)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…704)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The contract's nested-apply note said the v5 shape replaced a
"top-level error.code + string error pair"; the replaced pair was the
top-level errorCode. Three test doc comments still described the hosted
lock_held/lock_io envelope as a top-level errorCode with a string error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- get: the forced --id/--cve/--ghsa shape error no longer echoes the
  argument. With usage_error printing it, CodeQL traced a test's patch
  uuid into eprintln (rust/cleartext-logging).
- remove/scan tests: under --json the self-enforced usage error is now
  the coded error on stdout, so assert it there instead of on stderr.
- json_error_shape.rs spawns through hermetic::binary_command, as
  spawn_env_hygiene requires.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) force-pushed the arch-refactor/704-json-error-object branch from 70c3229 to a458ba9 Compare October 7, 2026 23:06
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] CI status: rebased onto main (05ecc6e) and force-pushed as a458ba9. One conflict, in CLI_CONTRACT.md's hosted-lock paragraph: kept main's new yarn-berry package.json gate sentence and applied this PR's error: {code, message} wording. Affected CLI tests pass locally (lib, json_error_shape, scan/get/remove/rollback/cli suites, hosted redirect/rollback, covgaps).

CI: 555 checks passed, 6 skipped. The first run was cancelled partway through and I re-ran it. One check is still red: composer 2.10.3 / php 8.5 / macos-latest. It fails in shivammathur/setup-php ("Could not setup PHP 8.5") before any repo code runs. It failed the same way 3 times here, and also on main (run 37708358429, e61a845) and on other PR branches, so it is an infra failure.


Generated by Claude Code

Resolve the CLI_CONTRACT.md and socket.yml policy test conflicts keeping
both sides: main's new hosted rows (vlt workspace roots, uv/Poetry
takeover gates, per-purl `patches`, path-flag validation) stay, with
top-level errors described as the {code, message} object. Point the new
vlt workspace-member test at error.message, fix the stale top-level
errorCode wording for eject_refused and managed_install, and note that
the pre-dispatch path-flag check, like clap's parse errors, prints
nothing on stdout under --json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 8, 2026 11:21
Resolve the CLI_CONTRACT.md hosted-scan paragraph conflict: keep main's
new gem refusal codes and describe lock_held / lock_io as the
error: {code, message} object.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Merged main, CI green; ready for review.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-cli/src/commands/rollback.rs
The path_glob_invalid usage error capitalized the message before handing
it to usage_error, so the --json error.message was capitalized too.
capitalize_first is a human-only transform: every other rollback JSON
error (emit_rollback_error) and scan's path_glob_invalid keep the
verbatim message. Capitalize only for the stderr line, and pin both
forms in the json_error_shape test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LD4qUfZKhg2qgt3x9vGeFf
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Resolve the conflicts with main's staged hosted takeover. hosted.rs now
writes through main's commit_hosted_writes; its failure path keeps the
PR's {code, message} error object and reports lockfile_write_failed.
CLI_CONTRACT.md takes main's staged-takeover text and changes the hosted
lock-failure JSON back to the PR's error object (no top-level errorCode).

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 8, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: labeled Ready for review at 6eb0448.

  • CI: 442/442 non-skipped checks green (ci-ok success), 14 skipped
  • Bugbot: reviewed 6eb0448, no unresolved findings.
  • Mergeable: yes, no conflicts with base.

Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 8, 2026
Main (#1043) moved get's agent-mode engine into agent_download.rs, so the
{code, message} error object is ported there: report_error takes a code,
report_lock_failure and the manifest read/write failures use
legacy_error, and fold_apply_failures sets the run-level error with
set_error_keep_status instead of a top-level errorCode. Rollback's
path_glob_invalid keeps the verbatim JSON message and uses main's
ui::sentence_case for the human line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9a15a0b. Configure here.

Main's new contract rows (workspace-lock-elsewhere, eject refusals) and
the stray-member-lock test (#1095) now use the top-level error object:
the rows say error.code, and the test reads error.message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-refactor PR opened by the scheduled architecture refactor routine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide: one shape for the --json top-level error (scan and get emit both a string and a {code, message} object)

4 participants