Skip to content

fix(scan): surface hosted redirect state in report-only scan --json and list - #220

Merged
Mikola Lysenko (mikolalysenko) merged 2 commits into
mainfrom
fix/scan-hosted-state-visibility
Aug 19, 2026
Merged

fix(scan): surface hosted redirect state in report-only scan --json and list#220
Mikola Lysenko (mikolalysenko) merged 2 commits into
mainfrom
fix/scan-hosted-state-visibility

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

fix(scan): surface hosted redirect state in report-only scan --json and list

Defect (gem live-matrix D3 — live-verified 2/2 per cell vs production)

After hosted wiring (scan --mode hosted) + a real install, the hosted state was invisible to both read-only surfaces a user would check, verified on three bundler majors against the production patch API (activestorage 6.0.3, hosted-wired Gemfile + patched gem on disk):

Container bundler report-only scan --json list --json
gem-b1 1.17 exit 0, zero hosted/redirect/warning fields exit 1 manifest_not_found
gem-b2 2.7 exit 0, zero hosted/redirect/warning fields exit 1 manifest_not_found
gem-b4 4.0 exit 0, zero hosted/redirect/warning fields exit 1 manifest_not_found

Root cause (at d9afb08, unchanged on main): hosted_wiring_retained is pushed to the scan JSON envelope only inside the --apply branch and the post-apply text path; the read-only path loads redirect_state but never emits it. And hosted mode records its patches only in .socket/vendor/redirect-state.json — it never writes .socket/manifest.json — so list hard-failed on a purely hosted project while its patches were demonstrably live. A hosted-wired project's report-only scan --json was byte-identical to a never-touched project's.

This closes the long-open "hosted-mode JSON gap" residual (scan/mod.rs review 2026-07-04); #207 surfaced the adjacent cross-mode warnings but not these two surfaces.

Fix design (both additive/MINOR per CLI_CONTRACT.md's own rules)

scan --json — new top-level redirectState block. Every non-hosted-mode, non-vendored-mode envelope (report-only, --mode agent/--apply/--sync, and the zero-discovery path) carries it whenever the redirect ledger holds ≥ 1 record:

"redirectState": {
  "mode": "hosted",                                  // constant label (a legacy ledger's own mode string may say "redirect")
  "ledger": ".socket/vendor/redirect-state.json",
  "records": [                                       // every ledger record, sorted
    { "purl": "pkg:gem/activestorage@6.0.3",         //   canonicalized — string-joins with wiringLive
      "ledgerKey": "pkg:gem/activestorage@6.0.3",    //   the ledger's verbatim key (may be %-encoded / ?qualified)
      "uuid": "" }
  ],
  "wiringLive": [ "pkg:gem/activestorage@6.0.3" ]    // purls this run counted whose live lock still proves hosted wiring
}

It is a descriptive state block, not a warning — a healthy hosted project's every scan must not read as an anomaly (the #207 hosted_wiring_retained warning stays exactly where it was: the agent-flow conversion-incomplete diagnostic, unchanged and now accompanied by the block). records are the ledger's word; wiringLive is the live lock's proof, computed ONCE per run by the same hosted_wiring_live gate as the #207 warning and scoped to this run's post---ecosystems-filter counted purls — a record with no proof means unwound wiring, an unreadable lock, or a purl this run did not cover, never "still live". Error envelopes (offline refusal, all-batches-failed) are deliberately minimal and never carry the block. Hosted-mode runs keep their redirect sub-object and vendored runs their takeover warnings; both can mutate the ledger mid-run, so neither duplicates a pre-run snapshot that could go stale.

list / list --json — hosted records fold in. Redirect-ledger records are listed alongside manifest entries, labeled apart: details.mode: "hosted" + details.ledger on the events (additive, absent on manifest entries; the constant mode name, whatever opaque mode string a pre-rename ledger carries), a Mode: hosted (recorded in .socket/vendor/redirect-state.json) line on the human path. Global purl sort, manifest entry before the hosted record on a tie — when both stores hold a purl, both are shown. Exit semantics: records found (either store) → exit 0; manifest_not_found still fires when neither store has a record (an edits-only ledger asserts no patches); a present-but-broken manifest still reports manifest_invalid/manifest_unreadable regardless of ledger records — corruption is never masked. A malformed ledger degrades to "nothing to consult" with a stderr warning, muted by --silent (the read-only consumer posture load_redirect_state documents; the hosted write path hard-errors instead). Both stores always come from the SAME project: with --manifest-path the ledger resolves against the root the resolved manifest path implies, never the local cwd. The patch_listed telemetry metric keeps its pre-hosted meaning (patches_count = manifest patches only).

Contract changes (CLI_CONTRACT.md)

  • scan section: new Hosted-state visibility (redirectState) paragraph; the read-only sentence now names the block.
  • Subcommands table: list row mentions the hosted ledger.
  • manifest_not_found row: list's hosted-only behavior + the never-masked corruption rule.
  • Per-subcommand action matrix: list's details.mode/details.ledger keys.

Tests (test-first — all RED against the old binary, GREEN now)

  • tests/scan_invariants.rs (+4, mirroring fix(scan): surface cross-mode state — hosted wiring retained, vendored ownership, berry vendored-entry refusal #207's fixtures): report-only block presence with records + live proof and no conversion warning; omission for edits-only/no ledger; records-vs-wiringLive split on a registry-clean lock; agent runs carry block + warning together.
  • tests/cli_parse_list.rs (+6 subprocess tests): hosted-only exit 0 with labeled events (JSON + human); manifest+ledger coexistence incl. a same-purl tie; edits-only ledger still manifest_not_found; corrupt manifest never masked; --silent suppression.
  • In-module unit tests: redirect_state_json gates/shape (scan), envelope labeling + hosted-only envelope (list).

Gates: cargo fmt --check on touched files, cargo clippy --workspace --all-features -- -D warnings (the exact CI invocation) clean, and the full scan_invariants, cli_parse_list, in_process_scan, in_process_redirect, in_process_redirect_pnpm, in_process_vendor, cli_parse_scan, cli_scan_silent, mode_migration_npm, mode_migration_cargo, repair_invariants, e2e_vex_redirect, e2e_safety_yarn_pnp, cli_parse_main suites plus the lib unit tests green. No rewriter touched, so the redirect goldens are unaffected.

Review round (adversarial review, applied in fa3a797)

An adversarial review produced convergent findings; all were reproduced RED-first and fixed:

  1. Vendored zero-discovery leak — the empty-crawl early-return gated redirectState on !hosted alone, so scan --mode vendored --json over an empty crawl carried the block. Gate fixed to !hosted && !vendor; omission now pinned for hosted AND vendored envelopes on both the zero-discovery and ≥1-package paths (hosted_mode_envelopes_omit_redirect_state, vendored_mode_envelopes_omit_redirect_state).
  2. --silent violations — the malformed-ledger degradation warnings (list + both scan consults) ignored --silent; all three now flow through one shared load_redirect_state_lenient(cwd, silent) (this also silent-gates the pre-existing main-path scan warning, per "errors only").
  3. records↔wiringLive joinrecords[] kept verbatim API-spelled ledger keys while wiringLive was canonical, so a live redirect of a %40scope%2F-encoded or ?platform=-qualified purl read as "wiring unwound" under the documented string-compare join. records[].purl is now canonicalized to wiringLive's spelling; the raw key is preserved as records[].ledgerKey.
  4. list --manifest-path store mixing — the ledger was resolved against cwd unconditionally, interleaving two projects' stores and letting a local ledger suppress the flagged project's manifest_not_found. The ledger now resolves against the project root the resolved manifest path implies.
  5. Telemetry redefinitionpatch_listed.patches_count had silently become "listed entries" (manifest + hosted, double-counting shared purls); restored to manifest-only, pinned by a wiremock body capture.
  6. Error envelopes — contract now states they are deliberately minimal and never carry redirectState.
  7. wiringLive scope — probes now use the post---ecosystems-filter scanned set; "purl not covered this run" documented as the third silent cause (GC prune deliberately keeps its pre-filter set).
    8-9. Efficiency/structure — the live-wiring probe (a full lockfile-inventory parse) runs once per run behind a new no-I/O gate, shared by the block and the agent warning; redirect_state_json is sync and takes the precomputed proof, its records gate being the block's single presence decision.
    10-13. Cleanups — one mode-label owner (HOSTED_MODE_LABEL; a legacy "redirect" ledger now still labels "hosted" on both surfaces), list exit predicate derived from entry emptiness, envelope-doc sort ownership fixed, manifest_envelope test helper, redirect-ledger fixture writer promoted to tests/common/mod.rs.

Possible follow-ups (out of scope here)

  • Report-only scan --json is similarly thin on vendored ownership (which packages the vendor ledger owns) — a separate observed gap, deliberately not addressed to keep the surface additive and reviewable.
  • Cross-command store-merge unification: list, vex, and scan's updates[] each fold manifest + redirect-ledger records with their own collision policy — real, but a core-level refactor.
  • remove/repair hosted-awareness asymmetry: repair has a redirect_only_project skip while remove still errors manifest_not_found on hosted-only projects.

🤖 Generated with Claude Code

…nd list

Live-verified defect (gem live-matrix D3, 2/2 per cell on bundler
1.17/2.7/4.0 vs production): after `scan --mode hosted` + install, the
hosted state was invisible to both read-only surfaces a user would
check —

* report-only `socket-patch scan --json` (exit 0) contained ZERO
  hosted/redirect/warning fields (byte-identical to a never-touched
  project's envelope): the `hosted_wiring_retained` warning rides only
  the agent-mode apply branch, and the `redirect` sub-object only a
  hosted-mode run's;
* `socket-patch list --json` exited 1 `manifest_not_found` — hosted
  mode records its patches ONLY in the redirect ledger and never writes
  `.socket/manifest.json`.

This matches the long-open "hosted-mode JSON gap" residual from the
2026-07-04 scan/mod.rs review; PR #207 surfaced the adjacent cross-mode
warnings but not these two surfaces.

Fix (both additive/MINOR per CLI_CONTRACT.md):

* scan --json: every non-hosted-mode, non-vendored-mode envelope
  (report-only, agent, and the zero-discovery path) now carries a
  top-level `redirectState` block whenever the redirect ledger holds
  records: `{ mode, ledger, records: [{purl, uuid}], wiringLive }`.
  It is a descriptive STATE block, not a warning — records are the
  ledger's word, `wiringLive` the live lock's proof (the same
  `hosted_wiring_live` gate as the #207 warning). Hosted-mode runs keep
  their `redirect` sub-object and vendored runs their takeover warnings
  (both can mutate the ledger mid-run, so a pre-run snapshot would go
  stale).
* list / list --json: hosted redirect-ledger records fold into the
  listing, labeled (`details.mode: "hosted"` + `details.ledger`; a
  `Mode: hosted` line on the human path), purl-sorted with the manifest
  entry first on a tie. A hosted-only project now exits 0 with its
  records; `manifest_not_found` still fires when NEITHER store has a
  record (an edits-only ledger asserts no patches), and a
  present-but-broken manifest still reports manifest_invalid /
  manifest_unreadable — corruption is never masked. A malformed ledger
  degrades to "nothing to consult" with a stderr warning (read-only
  consumer posture per load_redirect_state's contract).

Test-first: tests/scan_invariants.rs (4 new, mirroring the #207
fixtures) and tests/cli_parse_list.rs (6 new subprocess tests) were RED
against the old binary; plus in-module unit tests for the block builder
and the list envelope labeling. CLI_CONTRACT.md updated (scan section,
subcommands table, manifest_not_found row, list action-matrix row).

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

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.

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

Reviewed by Cursor Bugbot for commit cd7519a. Configure here.

Comment thread crates/socket-patch-cli/src/commands/list.rs Outdated
Comment thread crates/socket-patch-cli/src/commands/scan/mod.rs
…rfaces

Adversarial-review round on the redirectState/list-hosted work (PR #220),
all defects reproduced RED-first:

* vendored zero-discovery leak: the empty-crawl early-return gated the
  redirectState insertion on `!hosted` alone, so `scan --mode vendored
  --json` over an empty crawl carried the block the main path (and the
  contract) exclude — now `else if !vendor`, with omission tests pinning
  hosted AND vendored envelopes on both the zero-discovery and ≥1-package
  paths.
* --silent violations: the malformed-ledger degradation warnings (list +
  both scan consults) ignored --silent; all three now flow through one
  shared crate::commands::load_redirect_state_lenient(cwd, silent).
* records↔wiringLive join: records[] kept the ledger's verbatim API-spelled
  keys (percent-encoded scoped npm names, ?platform= qualifiers) while
  wiringLive was canonical — a live redirect read as "wiring unwound" under
  the contract's documented string-compare join. records[].purl is now
  canonicalized to wiringLive's spelling with the raw key preserved as
  records[].ledgerKey.
* list --manifest-path store mixing: the ledger was always resolved
  against cwd, interleaving two projects' stores and letting a LOCAL
  ledger suppress the flagged project's manifest_not_found — the ledger
  now resolves against the project root the RESOLVED manifest path
  implies (.socket parent's parent, else the manifest's directory).
* telemetry: patch_listed's patches_count had silently become "listed
  entries" (manifest + hosted, double-counting shared purls); restored to
  manifest patches only, pinned by a wiremock body capture.
* wiringLive scope: the probes now use the post---ecosystems-filter
  scanned set (a filtered-out purl is "not covered this run", never
  "unwound"); contract documents the third silent cause. GC prune
  deliberately keeps the pre-filter set.
* efficiency/structure: the live-wiring probe (full lockfile-inventory
  parse) runs once per run behind a new no-I/O gate and is shared by the
  block and the agent warning; redirect_state_json is now sync and takes
  the precomputed proof; the block's records gate is its single presence
  decision.
* one mode-label owner: crate::commands::HOSTED_MODE_LABEL feeds both
  scan's block (previously echoed the ledger's opaque mode string — a
  legacy "redirect" ledger now still labels "hosted") and list's details.
* list simplifications: exit predicate derived from combined_entries
  emptiness; build_list_envelope doc reflects the sort ownership;
  manifest_envelope test helper replaces the copy-pasted call sites.
* test fixture promoted: tests/common/mod.rs::write_redirect_ledger,
  used by cli_parse_list.
* contract: redirectState paragraph rewritten (canonical purl + ledgerKey,
  constant mode label, post-filter wiringLive + third silent cause,
  error envelopes excluded, --silent-gated degradation warning); list row
  gains the --manifest-path same-project scoping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 648f469 into main Aug 19, 2026
62 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/scan-hosted-state-visibility branch August 19, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants