Skip to content

v5: cap new patches per scan, most critical first - #294

Merged
Mikola Lysenko (mikolalysenko) merged 42 commits into
release/v5-prereleasefrom
v5/scan-limit
Sep 29, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 42 commits into
release/v5-prereleasefrom
v5/scan-limit

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Work item B of the staged-rollout design (docs/design/staged-rollout.md, §5, §7.2 B rows, §9.2). scan can now add a limited number of new patches per run, most critical first, and report the rest as deferred. Repeated scans roll a repo forward until everything is patched.

What changes

  • scan --max-new-patches <N|none> (env SOCKET_MAX_NEW_PATCHES, socket.yml patches.maxNewPatches). A run adds at most N patches to packages that had none in the project. 0 means upgrades only; none means no cap. Precedence is flag > env > socket.yml > unlimited.

  • Classification (§5.1). Every selected row is compared with the merged recorded view: agent manifest, then hosted lockfile pins, then vendor ledger.

    • ALREADY: the writer gets the recorded uuid, so a re-scan never swaps an applied patch for an equal sibling.
    • UPGRADE: the selection supersedes the recorded patch, judged by the new ranking::search_result_supersedes on by-package records. Never capped.
    • NEW: nothing is recorded. Capped.
  • Eligibility before budget (§5.2). A NEW row only takes a slot if it can land. Each mode uses its own write-free checks:

    • hosted: grants and purl/url, vlt preflight, takeover refusals, wheel metadata, and the rewrite's confirmation probe
    • vendored: the Bun/vlt preflight
    • agent: the vendored / not-installed partition

    A patch that can't land never blocks the queue. If a lookup fails under a cap, no NEW patch is admitted that run (rollout_incomplete_lookup).

  • Order. Eligible NEW packages are ranked in-flight first, then by severity, then advisory count (descending), then ecosystem, base purl and uuid. The order is total and uses no dates. The budget unit is the base purl, so qualifier twins and the same package in several roots cost one slot.

  • Budget scope. On disk, PATH directories share one budget, visited in sorted order. In memory, one budget covers every root.

  • Output.

    • Every successful scan --json result gets a top-level rollout block: maxNewPatches, counts, and a ranked deferred[].
    • Hosted mode mirrors deferred rows into redirect.skipped[] as rollout_deferred.
    • Human output gets a Rollout: line and next steps (Next up: …).
    • Exit codes don't change.
  • updates[] now comes from the by-package records, so it lists exactly the UPGRADE rows the run acts on. It falls back to the batch data only where no by-package offer exists.

  • In-memory engine (napi, hosted-bundle).

    • New options: maxNewPatches (a number or "none"), maxNewPatchesCap (a server ceiling that only tightens) and inFlightPatches.
    • The engine classifies each root against its committed .socket/manifest.json, its vendor ledger, and the hosted pins its own lockfiles name. It plans once across all roots and rewrites a root again without its deferred rows.
    • The result gains a session-level rollout block and ProjectResult.deferred[]. index.d.ts is updated.
  • Core: socket_patch_core::rollout holds plan_rollout, rollout_cmp, resolve_max_new and canonical_base_purl, with the §9.0 types.

Staged-rollout walkthrough

A repo with 9 patchable packages and --max-new-patches 3. This is the scenario tests/scan_rollout_e2e.rs runs in hosted, agent and vendored mode:

Run Adds (most severe first) rollout.counts Deferred (ranked)
--dry-run nothing written; same decisions as run 1 {new: 3, deferred: 6, upgrade: 0, already: 0} c, d, h, a, i, f
1 e (critical ×2), b (critical), g (high ×3) {new: 3, deferred: 6, upgrade: 0, already: 0} c, d, h, a, i, f
2 c (high), d (medium), h (medium) {new: 3, deferred: 3, upgrade: 0, already: 3} a, i, f
3 a (low), i (low), f (unknown) {new: 3, deferred: 0, upgrade: 0, already: 6} —
4 nothing; lockfile byte-identical {new: 0, deferred: 0, upgrade: 0, already: 9} —

Human output for run 1 (stdout; warnings go to stderr):

Redirected 3 packages; rewrote 2 files.
Rollout: 3 of 9 new patches applied (maxNewPatches=3 from --max-new-patches); 0 upgrades, 0 already applied.
Commit .npmrc and package-lock.json to keep the redirect.
Reinstall from the updated lockfile (e.g. `npm ci`) so the installed packages pick up the patched artifacts, then run `socket-patch vex` to verify them.
6 new patches deferred; commit these changes and run scan again to apply the next 3.
Next up: roll-c@1.0.0 (high), roll-d@1.0.0 (medium), roll-h@1.0.0 (medium), …

After that, a newer superseding patch for an applied package still lands with --max-new-patches 0 (hosted and agent), and it shows up in updates[].

With socket.yml instead of the flag:

version: 2
patches:
  minSeverity: high
  maxNewPatches: 2   # a PR bot keeps the same 2 until merged, then proposes the next 2

Decisions where the plan left a gap

These are also recorded in docs/design/staged-rollout.md §11:

  • A JSON report-only run fetches no by-package data. Its updates[] stays on the batch rungs, and its rollout block has zero counts.
  • Discovery only recognizes pins on patch.socket.dev or a --patch-server-url origin. A hosted pin on any other server would read as NEW on every run and stall the rollout. To prevent that, a NEW row counts as ALREADY when a lockfile names its selected uuid. This uses one linear uuid scan per file.
  • The in-memory engine has no disk discovery, so it reads pins as the offered uuids each root's own files mention. A pin to a patch the API no longer offers counts as NEW once.
  • The in-memory maxNewPatches option reports source: "flag".
  • A capped reference-lookup failure that only affects NEW rows defers them with rollout_reference_failed instead of failing the run.
  • The Rollout: line only prints when a cap is set. A wet hosted run that can only defer takes no apply lock and writes nothing.

Self-review

Five parallel reviewers (correctness, determinism/churn, UX/output, test quality, perf) went over the diff. Fixed from their findings:

  • the recorded view now folds case like discovery and keeps every hosted pin, so a NuGet pin or two pinned pypi twins no longer reads as NEW or as a phantom upgrade
  • O(1) lookups replace per-row scans
  • the lock is taken for rows an earlier directory admitted
  • error envelopes drop rollout
  • the in-memory re-rewrite keeps its first-pass skips, and a fully failed root freezes new admissions
  • dry-run, incomplete-lookup, zero-cap and shared-budget wording is fixed
  • the flag no longer reads the env value
  • more tests

Left as documented limits (§11):

  • memory refuses a root whose symlinked file only a deferred row touches
  • a single-pass optimistic rewrite (perf) is not done yet
  • no vlt-withheld e2e case

Integration with work item A

  • Work item A landed as v5: socket.yml patch rollout config and filtering #293, which loads the socket.yml policy in scan and the in-memory engine. This branch is merged up to it.
  • The file layer is live. SelectionPolicy::max_new_patches() is the socket.yml layer of the cap on disk (both run_scan and the PATH-list budget), and in memory it is resolved after the session's socket.yml loads. --no-socket-yml drops it, and a flag or env cap still wins.
  • Pipeline order (§9.0). A's ScanPolicy::select (root, package and severity filters, retained set) runs inside discover_selected. The rollout classifies what it keeps: a retained package is neither NEW nor UPGRADE, and a severity floor removes a NEW row before it takes a slot. The memory engine's select_with_policy now returns Offers and feeds the same classification.
  • A memory session that fails on its socket.yml (policyError) has no rollout block (index.d.ts: rollout?).
  • Combined e2e (§9.3) in tests/hosted_memory_rollout.rs, on disk and in memory:
    • fixture: includePaths: ["/apps/"], minSeverity: high and maxNewPatches: 2 over apps/one, apps/two and legacy
    • both engines converge in two runs and never touch legacy/
    • then --no-socket-yml --max-new-patches 1: disk spends the slot in directory order, and memory spends it run-wide on legacy's critical patch
  • A has landed (v5: socket.yml patch rollout config and filtering #293), so this PR's diff against release/v5-prerelease is work item B only.

Tests

  • Core unit tests: rollout_cmp sorts every permutation the same way; plan_rollout with caps 0 / 1 / more than available, ties across ecosystems, carry across directories, ineligible rows, incomplete lookups, in-flight rows and twins; the resolve_max_new precedence table (including a cap on none); search_result_supersedes rungs.
  • CLI unit tests: classification rows, the recorded-view matching rules, the rendered block and lines, the uuid scanner.
  • E2E (tests/scan_rollout_e2e.rs):
    • a failed reference lookup defers new rows under a cap and still fails uncapped or with applied rows
    • a failed lookup for an already-applied package does not freeze new patches
    • three runs converge in hosted, agent and vendored mode, and a fourth changes nothing
    • dry run equals wet run
    • an upgrade lands with a cap of 0
    • withdrawn and bad_purl top-ranked patches hold no slot
    • a failed detail lookup admits nothing new
    • PATH directories share one budget
    • the env/flag precedence and a malformed env value (exit 2)
    • convergence without --patch-server-url
  • Parity and policy (tests/hosted_memory_rollout.rs):
    • disk and memory produce identical rollout and redirect blocks and identical bytes run after run until converged
    • two roots: memory's run-wide ranks vs disk's per-directory budget
    • memory counts a committed manifest, vendor ledger or pin as recorded
    • in-flight patches go first; the server cap tightens the limit
    • socket.yml policy plus cap converge on disk and in memory (§9.3)
  • Parser contract rows in cli_parse_scan.rs, and a napi smoke test for maxNewPatches.
  • cargo clippy --workspace --all-features --all-targets -- -D warnings is clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when and which lockfiles/manifests scan mutates across hosted, agent, and vendored modes; misconfigured socket.yml or rollout caps could defer critical patches until later runs, though policy only narrows and invalid config fails closed.

Overview
scan gains staged rollout and repo-root socket.yml patch policy, so teams can narrow what gets patched and drip new fixes in by severity instead of landing everything in one run.

Gradual rollout (--max-new-patches, env, patches.maxNewPatches). After policy-aware selection, each package is ALREADY, UPGRADE, or NEW against the merged recorded state (manifest → hosted pins → vendor ledger). Only NEW rows consume a shared per-invocation budget, ranked by severity and advisory count; upgrades and re-confirms are uncapped. Hosted mode defers after eligibility checks (including a confirmation rewrite probe), mirrors deferrals in redirect.skipped[] and JSON rollout; agent/vendored and the in-memory engine (hosted-bundle, napi) follow the same planner. Failed lookups under a cap defer all new patches (rollout_incomplete_lookup). get ignores the cap and warns when policy would have filtered packages.

socket.yml policy (integrated with rollout). Repo-root patches / projectIgnorePaths narrow paths, ecosystems, packages, severity floor, enabled, and maxNewPatches; invalid or ambiguous files fail scan before I/O (socket_yml_invalid / socket_yml_ambiguous, --no-socket-yml bypass). Discovered projects skip default test/fixture trees; explicit PATHs and negation patterns can re-include. Successful scan --json adds policy and rollout blocks; human output adds policy and rollout lines.

Patch retention / updates[]. Re-scans keep the recorded patch unless the offer meaningfully supersedes it (ranking::search_result_supersedes on by-package records); updates[] tracks the upgrades the run actually applies.

Other: ignore + serde-saphyr for YAML/glob matching; CI test-release timeout 40m → 50m; docs (README, CLI_CONTRACT, CHANGELOG).

Reviewed by Cursor Bugbot for commit 03aed58. Configure here.

Adds the design for rolling Socket patches out gradually: a
`patches:` block in socket.yml that narrows what scan may patch
(paths, ecosystems, packages, severity floor, on/off), and a
severity-ordered per-run cap on new patches so each scan lands the
next few most critical fixes.

The plan splits the work into two parallel items with a frozen
interface, lists every hard-coded filter and where it belongs, and
covers the depscan autopatch follow-up. configuration.md now records
that socket-patch reads socket.yml for selection policy only, with
the trust boundary unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three independent reviews (ambiguity, churn, trust boundary) found
gaps that would have let the two implementations disagree or let a
repo file widen or stall the rollout. The plan now:

- matches paths against marker files with the backend's top-down
  gitignore rules, so projectIgnorePaths means the same everywhere
- uses one data source for severity, supersession and ordering
- spends the budget only on patches the planning pass proves can
  land, and admits nothing new when a lookup failed
- uses the merged recorded view in every mode and engine
- has depscan read the policy from the base commit for PR jobs
- hardens file handling (regular files, aliases, size, encoding,
  trusted repo root) and reports what a policy hides

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A final consistency pass found places where the two work items
would have produced incompatible code: base purls admitted in one
directory being charged again in the next, no defined hand-off of
the unfiltered offers from the severity filter to classification,
no shared repo-relative path helper, and override sources the JSON
must report but the interface could not carry. The shared contract
now defines each of these, and the parity tests match each engine's
budget scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The plan said both that the selector returns the shared offers
struct (work item A) and that it returns admitted/deferred rows
(work item B). The selector now returns the offers, and B adds the
rollout stage after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new core module plans a capped scan: rows that already carry a
patch always go through, NEW packages are admitted most critical
first until the budget is spent, and the rest are deferred with
their rank. The order is total and uses no dates, so repeated scans
on an unchanged repo land the same patches and converge.

ranking gains search_result_supersedes, the by-package twin of
batch_supersedes, so ALREADY vs UPGRADE is judged on the same
records that pick the patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New socket_patch_core::policy module: the SelectionPolicy, Offers and
repo-root helpers that the staged-rollout plan freezes as the shared
contract between the socket.yml work and the --max-new-patches work.

It reads the repo root's socket.yml/socket.yaml strictly and fails
closed: bad YAML, a misspelled `patches` block, unknown keys (with a
did-you-mean hint), wrong types, empty allowlists, bad globs and
anchors or aliases inside the keys we read are all errors that name
the key path. Path lists match marker files with npm `ignore`
semantics, walked top-down; a golden fixture generated from the npm
package pins that. The built-in test/fixture ignores become
overridable defaults for discovered roots.

--package matching moves to core so scan and the policy share it.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan gains --max-new-patches <N|none> (env SOCKET_MAX_NEW_PATCHES).
Each selected package is classified against the recorded state
(manifest, hosted pins, vendor ledger): packages that already carry a
patch always go through, and an already-applied patch is kept unless
the selection really supersedes it, so re-scans never swap patches.
Packages getting their first patch are admitted most severe first
until the budget is spent; the rest are deferred to the next scan.

Hosted, vendored and agent runs decide eligibility with their own
write-free checks before any budget is spent, so a patch that cannot
land never holds a slot. Several PATH directories share one budget.
--json reports a rollout block, deferred rows show up in
redirect.skipped[] as rollout_deferred, and human output adds a
Rollout line and a Next up list. updates[] now uses the by-package
records.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nine packages under a cap of three roll forward three per run in
hosted, agent and vendored mode, most severe first, and a fourth run
changes nothing. The dry run predicts the wet run, upgrades land with
a cap of zero, patches that cannot land hold no slot, a failed lookup
admits nothing new, and several project directories share one budget.

A hosted pin on a patch server scan does not recognize still counts
as applied when the lockfile names its patch uuid, so the rollout
cannot stall on a missing --patch-server-url.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan now reads the repo root's socket.yml before any write and applies
its patches block in hosted, vendored and agent mode, --dry-run
included: path filters on project roots (PATH-glob matches also get
the built-in test/fixture ignores), ecosystem and package filters on
crawled packages, and a severity floor on the patches a package may
receive. An invalid or ambiguous file fails the run with exit 1 and
an errorCode before any request.

Packages that already carry a patch are never removed, upgraded or
replaced by the policy: they are held and reported under
policy.retained. A recorded patch below a new floor stays in place.
patches.enabled: false reports what would be patched and writes
nothing.

New flags: --no-socket-yml / SOCKET_NO_SOCKET_YML and
--min-severity / SOCKET_MIN_SEVERITY. Every successful scan --json
result gains a top-level policy block. A PATH outside the repository
root is a usage error.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Builds on A's shared step 5 -> 7 seam: the scan stage now fills
policy::Offers and reports project paths with policy::repo_relative.
The socket.yml maxNewPatches layer stays unwired until A loads the
policy in scan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The napi engine and hosted-bundle take maxNewPatches (a count or
"none"), maxNewPatchesCap (a server ceiling that only tightens) and
inFlightPatches (ranked first). One budget spans every project root:
the engine classifies each root against its committed manifest,
vendor ledger and the hosted pins its lockfiles name, plans once
after every root's write-free checks, and rewrites a root again
without its deferred rows. Results gain a session-level rollout
block and ProjectResult.deferred; deferred rows also appear in
skipped[] as rollout_deferred.

Parity tests hold disk and memory to the same rollout and redirect
blocks run after run until converged, and show memory's run-wide
budget beside disk's per-directory one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CLI_CONTRACT.md gains the limit's semantics (classification,
eligibility, unit, budget scope, order, convergence), the flag and env
rows, the rollout block, rollout_deferred and a jq recipe; "Which
patch gets selected" now describes the by-package supersession and the
separate cross-package order. README adds a gradual-rollout task and
the flag row; CHANGELOG adds both entries. The design doc records the
gaps B decided.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
selectHostedScanPaths now streams the root socket.yml/socket.yaml and
returns them as policyPaths; it drops test and fixture trees through
the policy's built-in default ignores instead of a hard-coded segment
list (structural excludes like node_modules and vendor stay fixed).

The session reads the policy before any root is processed: path
filters run before the project limit, ecosystem and package filters
on each root's packages, and the severity floor before selection. A
listed policy file that arrives without content, or an invalid one,
yields policyError with no root processed and no file changed. New
options noSocketYml, minSeverity and policyPaths; the result gains a
policy block. hosted-bundle and index.d.ts carry the new fields.

get now warns policy_bypassed when the repo's socket.yml would have
skipped the package it patches.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CLI_CONTRACT.md gains a "socket.yml patch policy" section (grammar,
precedence, paths, lookup, validation, commands, error codes and the
policy JSON block), the flag and env rows, and the "narrow or pace"
half of the trust-boundary rule. README adds a "Roll out gradually"
section with copyable socket.yml recipes, CHANGELOG lists the new
policy and the breaking scan changes, and the design docs record the
decisions made while building it.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The recorded view is now an index built once per project and keyed
like discovery (case-folded nuget/composer, PEP 503 pypi names), and it
keeps every hosted pin, so a case-folded pin or a pair of pinned
qualifier twins no longer reads as NEW or as a phantom upgrade. The
hosted gate uses key sets instead of linear scans, takes the apply lock
for rows an earlier directory admitted, and error envelopes drop the
rollout block. The in-memory engine keeps the first pass's skips when
it rewrites a root again, and a root whose lookups all failed freezes
new admissions. Human output words dry runs, incomplete lookups, zero
caps and shared budgets correctly, an explicit flag no longer reads
the env value, and the docs say the socket.yml layer arrives with the
patch policy. New tests cover reference failures, upgrades under a cap
of zero in hosted mode, recorded packages with failed lookups, and
stronger dry-run and parity checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes from an adversarial self-review of the policy work:

- A non-ASCII package spec no longer panics validation, and error
  text, warnings and verbose lines drop terminal escapes and bidi or
  zero-width characters.
- Ignore lists that fail to compile together are an error instead of
  silently matching nothing, and the policy file is opened by its
  resolved path without following a swapped-in symlink.
- A top-level key that looks like a misspelled `patches` (`patchs`),
  a top-level merge key or an aliased key now fails closed.
- Repo-root lookup follows a `.git` symlink and trusts the checkout
  owner under root and sudo, so CI containers keep the policy.
- The severity floor always reports the patch it held back, report-
  only --json runs report what a floor or `enabled: false` hides, a
  root skipped as a whole is always one entry and no longer prints
  "No packages found", warnings print once per invocation, and the
  policy line is omitted when there is nothing to say.
- policy entries are sorted and use canonical purls on disk and in
  memory; the in-memory engine applies a socket.yml negation of a
  built-in ignore to roots it was given, and policyError carries no
  CLI-only remedy.
- A lockfile-less disk root matches path filters by its manifests.

Tests cover each fix, plus the prune universe, agent path filters, a
vendored package held byte-identical, and tighter oracles; docs are
corrected where they overstated what the human output names.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The dry-run tense fix changed the deferred line to "would be
deferred"; the unit test still expected the wet-run wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in work item A at faa6ffc: scan and the in-memory engine now
load the repo's socket.yml patch policy. The rollout classifies what
the policy keeps, so a held package is never NEW and a severity floor
removes a patch before it takes a slot.

socket.yml patches.maxNewPatches now sets the per-run cap when no
flag or SOCKET_MAX_NEW_PATCHES is given; --no-socket-yml drops it.
The in-memory engine resolves the cap after the session's socket.yml
loads. A combined test runs the policy and the cap together on disk
and in memory until both converge.

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

Mikola Lysenko (mikolalysenko) commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

[agent] CI status (updated for 1613348): complete and green except the 8 jobs that also fail on the base branch. See the latest [agent] ready to land comment for the per-check base-run links.


Generated by Claude Code

The optimized test job now has to build two more crates and one more
test binary for the socket.yml policy. It ran out of time on its last
40 minutes, about a minute short, and a docs-only change already takes
38. Raise the limit to 50 minutes so it can finish.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Picks up A's CI change that gives the release test job more time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in the merged staged-rollout plan (#290), the Windows scan
test fix (#288) and the docs-only design PRs. The plan's late
two-phase memory selection is implemented in the next commit.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in the squashed staged-rollout plan (#290), the e2e-tier fix
(#288: Windows path separators in a scan test, CI cancel rules) and
the docs-only design PRs. The plan doc's later edits (two-phase
policy selection for the in-memory engine) merge with this branch's
implementation notes unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The merged plan makes in-memory path selection two-phase: the host
fetches the root socket.yml first and passes its text to
selectHostedScanPaths. Selection now applies the full path policy, so
a negation such as `!/e2e/tests/` brings a test tree back in memory
exactly as on disk. A listed policy file that is missing, symlinked
or invalid returns policyError with nothing selected.

Selection returns policySha256, and the session fails closed when the
policy it reads differs. Roots the policy excludes keep their marker
files presence-only, so the session still lists them as filtered.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-ups to two-phase selection:

- Roots the policy excludes are reported in ignoredSample instead of
  streamed presence-only, so a repo with many fixture lockfiles no
  longer runs into the session's file limit.
- A session that bypasses socket.yml while selection applied it now
  fails closed instead of processing roots it never fetched.
- The docs say policy text must decode losslessly (TextDecoder drops
  a BOM) and when policySha256 is null.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in 28cebf7, which fixes the hosted rollback failures the vlt
compatibility legs hit on this branch.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in A's two-phase memory selection (the host passes the root
socket.yml text to path selection, and the session requires the
policySha256 selection returned) and the base's ledger-free hosted
rollback fix for vlt pins and uv overrides.

The combined socket.yml + cap test now runs the memory engine through
path selection first, as a host with a root socket.yml does.

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

Copy link
Copy Markdown
Collaborator Author

#281 landed on release/v5-prerelease as 73c0c4f; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

Brings in #279, which removes `setup` and streamlines the patch UI.
Scan keeps `selection_args`, which `get` now calls again. The exit-2
row of the CLI contract keeps the removed-`setup` wording and the
v5.0 socket.yml and SOCKET_MAX_NEW_PATCHES usage errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in #279 (setup removed, patch UI streamlined). Conflicts:
- core lib.rs keeps `policy` and drops `setup`.
- scan keeps policy-aware selection (`select_accessible`) and the
  base's `selection_args`, which `get` now uses.
- CLI_CONTRACT exit-code rows take the base's text plus the
  socket.yml rows.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The v5 help rules cap each command's short help at about eight
options. `--no-socket-yml` and `--min-severity` pushed `scan -h` to
ten, so they now appear only in `scan --help`, like the other
advanced scan flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
The staged-rollout and socket.yml flags pushed scan's short help past
nine options once #279 trimmed -h. --max-new-patches and
--no-socket-yml now show only in --help and parse as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in A's own short-help trim. scan -h now hides --no-socket-yml,
--min-severity and --max-new-patches, leaving eight options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A report-only `scan --json` (`--prune` with no mode) with a severity
floor or `enabled: false` fetches patch details to fill
`policy.filtered[]`. If every query failed it still printed a success
envelope and exited 0. It now reports the error and exits 1, like the
agent and vendored runs.

Human-mode policy warnings now print `Warning: <detail>` like the rest
of `scan`; the code stays in the JSON `warnings[]` entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

#282 landed on release/v5-prerelease as 14a9cb0; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Merging #282 (14a9cb0) is in progress. I'm doing it after A (#293) merges it, not before.


Generated by Claude Code

Brings in #282, which moves the in-memory engine into
socket-patch-core (`hosted::memory`). The two-phase policy selection
moves with it. Conflicts:
- `canon`, `FilteredEntry`, `RetainedEntry` and `policy_block` move
  from the CLI into `socket_patch_core::policy`, so the core engine
  and disk scans share one `policy` block.
- `roots` is now public in core, for disk scans' marker lookup.
- scan reads the ledgers and wiring through the base's
  `ProjectContext`, and keeps loading the recorded view before
  filtering.
- CLI_CONTRACT's hosted-bundle row takes the base's path, plus the
  policy fields.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Brings in #282 through A: the in-memory engine now runs the disk
engine's stages in socket_patch_core::hosted::memory.

The rollout moves with it. The stage, the recorded view and the
classification live in socket_patch_core::rollout::stage, and the
ledger fold for updates lives in socket_patch_core::ledgers, so disk
and memory share them. scan keeps updates[], the hosted gate and the
human lines. On disk the gate plans after the engine's first rewrite
and rewrites again without the deferred rows; in memory each root
keeps its plan for that second pass.

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

Copy link
Copy Markdown
Collaborator Author

#291 landed on release/v5-prerelease as f9cb7e1; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

Brings in #291, which builds the e2e binaries once per OS and tiers
the package-manager matrix.

The release test job keeps the base's 40-minute limit. This branch
raised it to 50 because the job used to build the test graph twice;
it now builds once, so the extra time is no longer needed.

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

Copy link
Copy Markdown
Collaborator Author

[agent] coverage on a6f5008 hit its 35-minute timeout. The cause is a test flake in code this PR doesn't touch, so I've re-run the failed jobs once (run 36497894523).

What hung: vendor::maven_repo::tests::fifo_project_pom_fails_fast_in_revert (log: "has been running for over 60 seconds", then cancelled at 35 minutes).

Why its guard hung too:

  • expect_fast in crates/socket-patch-core/src/vendor/maven_repo.rs gives the future 5 seconds.
  • When time runs out, it opens the FIFO for writing so a stuck reader can finish. That open doesn't pass O_NONBLOCK.
  • revert_maven reads the pom with read_regular_to_string, a guarded non-blocking read, so no reader ever waits on the FIFO. The watchdog's write-open then blocks forever instead of failing the test.
  • Result: a revert that takes a little over 5 seconds on a loaded coverage runner turns into a hang that uses up the whole job.

Not from this PR: coverage passed on 2270cb3 in 13 minutes and on the base branch at f9cb7e1 in 11 minutes, and this PR doesn't change vendor/.

Proposed fix (not pushed here, since it's outside this PR's scope). It mirrors release_fifo_reader in cargo_config.rs:

             Err(_) => {
-                let _ = std::fs::OpenOptions::new().write(true).open(fifo);
+                use std::os::unix::fs::OpenOptionsExt;
+                let _ = std::fs::OpenOptions::new()
+                    .write(true)
+                    .custom_flags(libc::O_NONBLOCK)
+                    .open(fifo);
                 panic!("{what}");

With it, a slow revert fails the test after 5 seconds instead of hanging the job. The same re-run also repeats the 8 jobs that also fail on the base branch (4 vlt install-proof, 4 yarn-classic). All other checks on a6f5008 passed, including install-proof (ubuntu-latest, 1.0.0-rc.22).


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

#292 landed on release/v5-prerelease as a7b0d00; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

Brings in #292: vendored runs ask for every planned package's
download reference in one request, and a patch that creates a file
now fetches that file's blob even when its diff archive is cached.

The only conflict was two new CHANGELOG entries in the same place;
both are kept.

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

Copy link
Copy Markdown
Collaborator Author

#296 landed on release/v5-prerelease as 1e3ace6; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

Brings in #296, which removes dead code and the v3 compatibility
shims.

#296 made MemoryProject::entries() test-only because nothing else
called it. This branch's in-memory rollout does: it reads every text
file in the project to find the patches it mentions. The method stays
crate-visible outside tests.

In CLI_CONTRACT.md the exit-code rows keep both sides' changes: #296
drops the --detached and --one-off entries, and this branch adds the
socket.yml, scan PATH and SOCKET_MAX_NEW_PATCHES entries.

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

Copy link
Copy Markdown
Collaborator Author

#297 landed on release/v5-prerelease as b97a1c2; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

Brings in #297, which groups the CLI tests into one binary per command
and replaces the oracle comparisons with golden files. The merge had
no conflicts. This branch's own suites stay separate binaries.

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

Copy link
Copy Markdown
Collaborator Author

#293 landed on release/v5-prerelease as b9e106d; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready.


Generated by Claude Code

Brings in #293, the socket.yml rollout config this branch builds on.
This branch already carries every change #293 made, so every conflict
resolves to this branch's side and the tree matches the previous head
exactly. No code changed.

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

Copy link
Copy Markdown
Collaborator Author

Coordinator: #294 is next in the landing order but is blocked. Its only [agent] ready to land comment (2026-09-28 18:24Z) is older than the last push (1613348, which merges b9e106d / #293, at 2026-09-29 10:22Z). CI on 1613348 was also still finishing when I checked. Once CI completes, please post a fresh [agent] ready to land for 1613348 that lists any red checks and the base-branch run where each one also fails.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] ready to land

  • Head: 1613348, which merges release/v5-prerelease at b9e106d (v5: socket.yml patch rollout config and filtering #293, work item A). Against the base, the diff is work item B only (48 files).
  • What B adds: scan --max-new-patches <N|none> (env SOCKET_MAX_NEW_PATCHES, socket.yml patches.maxNewPatches). Each run adds new patches most severe first, never caps upgrades, and converges over repeated runs. Results are reported in the rollout block, on disk and in the in-memory engine.
  • CI on 1613348 is complete: 415 passed, 6 skipped, 8 failed. coverage and Pipenv compatibility passed. Each of the 8 failures also fails on the base branch:
Red check on 1613348 Same check failing on release/v5-prerelease
install-proof (ubuntu-latest, 0.0.0-1) run 36454568301 @ 28cebf7
install-proof (ubuntu-latest, 0.0.0-1, 22.0.0) run 36454568301 @ 28cebf7
install-proof (ubuntu-latest, 0.0.0-11) run 36454568301 @ 28cebf7
install-proof (windows-latest, 0.0.0-11) run 36454568301 @ 28cebf7
yarn-classic 1.0.2 run 36554947505 @ b9e106d
yarn-classic 1.6.0 run 36554947505 @ b9e106d
yarn-classic 1.7.0 run 36554947505 @ b9e106d
yarn-classic 1.9.4 run 36554947505 @ b9e106d
  • Base evidence for the vlt jobs: the vlt compatibility workflow didn't run for b9e106d. Its latest run on release/v5-prerelease is 36454568301 (a workflow_dispatch at 28cebf7), and it has the same four install-proof jobs red. This PR doesn't touch vlt rollback or yarn-classic.
  • Reviews: there are no review threads or reviews on the PR.

Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 180f10f into release/v5-prerelease Sep 29, 2026
437 of 445 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the v5/scan-limit branch September 29, 2026 12:18
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