Skip to content

feat(check): materialize absent Yarn candidates in an isolated mirror (v2.11.0) - #18

Merged
aldegad merged 3 commits into
mainfrom
plan/yarn-candidate-closure-materialization
Jul 24, 2026
Merged

feat(check): materialize absent Yarn candidates in an isolated mirror (v2.11.0)#18
aldegad merged 3 commits into
mainfrom
plan/yarn-candidate-closure-materialization

Conversation

@aldegad

@aldegad aldegad commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Problem

v2.10 resolved a Yarn Berry project's closure from its actual yarn.lock, but only for a package already in that lockfile. Checking a dependency before adding it — the case the gate exists for — hit project-closure-unavailable and became deny-only. The normal release path for a new Yarn dependency was blocked even when the project's own resolutions would have resolved it safely.

This was the reproducible reason the validator rejected the v2.10 plan.

Approach

When the requested locator is absent from the project lockfile, resolve the candidate in an isolated mirror instead of denying or falling back to the published closure.

  • Private mirror. mktemp copy of the canonical resolution inputs only: root + workspace package.json, yarn.lock, .yarnrc.yml, .yarn/{releases,plugins,patches}. node_modules, caches, unplugged packages, install state, and VCS data are never copied.
  • Candidate added to the mirror manifest alone, then resolved with yarn install --mode=update-lockfile --no-immutable. That documented mode skips the link step, so no candidate lifecycle script runs.
  • Caller invariance. The caller's tree is read-only throughout. Inputs are re-hashed before and after the Yarn run; a concurrent manifest/resolutions/config/lockfile edit invalidates the candidate rather than approving a mixed project state.
  • Provenance-bound approval. project_context.type becomes yarn-project-materialized-lockfile and carries materialization { candidate, input_sha256, generated_lockfile_sha256, command, isolation }. safedeps_ledger_validate_json requires every field and rejects an entry whose materialization.input_sha256 disagrees with the context input_sha256.
  • No fallback. Any failure (input copy, mirror/canonical hash mismatch, Yarn invocation, unresolvable candidate) denies with project-candidate-materialization-unavailable. The published closure is never used as a substitute.

Approval truth is therefore neither a registry probe nor the caller's stale lockfile: it is the Yarn resolution derived from a hash-bound copy of the caller's own inputs.

Verification

  • npm test (smoke + e2e) green, including the new Yarn absent candidates materialize only in an isolated mirror with bound provenance
  • hermetic fixture: next@16.2.11 approves only when the isolated closure resolves sharp@0.35.3 + postcss@8.5.21; the unpatched sharp@0.34.5 / postcss@8.4.31 closure denies
  • unavailable materialization denies with no ledger approval and no published probe; changed input/lock context denies
  • caller tree and lock hashes byte-identical before/after; nested node_modules asserted absent from copied mirror inputs
  • bash -n on all touched scripts, git diff --check, version SSoT 2.11.0 (package.json == SAFEDEPS_VERSION), safedeps scan secrets --repo clean
  • docs consistency audit per AGENTS.md: no Korean prose in English docs, English/Korean mirrors symmetric

Invariants held

Zero npm dependencies. OSV remains the single canonical advisory truth. effect-primary stays npm-only. No silent fallback — every failure path is an observable deny.

Docs

English SSoT + Korean mirrors updated (README, ARCHITECTURE, ROADMAP, SKILL). ROADMAP had no v2.10 section at all — the previous release bumped only the current-version line — so this adds the missing v2.10 entry alongside v2.11 in both languages.

aldegad added 3 commits July 24, 2026 18:03
v2.10 could only judge a package already present in the project's
yarn.lock, so checking a dependency before adding it fell to
project-closure-unavailable and became deny-only. That blocked the
normal release path for a new Yarn dependency even when the project's
own resolutions would have resolved it safely.

When the requested locator is absent, safedeps now builds a private
mktemp mirror containing only the canonical resolution inputs (root and
workspace package.json, yarn.lock, .yarnrc.yml, .yarn/releases|plugins|
patches), adds the candidate to the mirror manifest alone, and resolves
it with `yarn install --mode=update-lockfile --no-immutable`. That mode
skips the link step, so no candidate lifecycle script runs. node_modules,
caches, unplugged packages, install state and VCS data are never copied.

The caller's tree stays read-only. Project inputs are re-hashed before
and after the Yarn run, so a concurrent manifest/lockfile edit
invalidates the candidate instead of approving a mixed project state.

The approval binds its provenance: project_context becomes
yarn-project-materialized-lockfile and carries materialization
{candidate, input_sha256, generated_lockfile_sha256, command, isolation}.
Ledger validation requires those fields and rejects a materialization
input hash that disagrees with the context hash.

Every failure path (input copy, context drift, Yarn invocation,
unresolvable candidate) denies with
project-candidate-materialization-unavailable. There is no
published-closure fallback.
Cover the private-mirror-only materialization, the canonical input set,
the input/generated-lock provenance binding, the fail-closed denial with
no published-closure fallback, and caller-tree invariance across the
English SSoT and the Korean mirrors.

ROADMAP had no v2.10 section at all (the previous release bumped only
the current-version line), so this adds both the missing v2.10
resolution-aware entry and the new v2.11 entry, in both languages.
…rn call

CI caught this on ubuntu while macOS passed. The caller-project assertion
compared the fixture's logical path against the $PWD the Yarn stub logs,
which safedeps has resolved with `pwd -P`. On macOS TMPDIR lives under the
/var symlink, so the two strings never matched and the assertion passed
without testing anything. On Linux /tmp is a real directory, the strings
matched, and it failed.

The assertion was also wrong on its own terms. Locator discovery runs
`yarn info` in the caller project by design -- that is the v2.10
project-closure read -- so forbidding every Yarn invocation there
contradicts the feature. What must never happen is a *mutating* command
in the caller.

Compare physical paths and assert the real property: no install-class
command runs with the caller as cwd, and the update-lockfile install does
run somewhere that is not the caller. The pair cannot both hold unless the
materialization was isolated, so neither half can pass vacuously.

Verified by mutation: forbidding `info` in the caller now fails on both
macOS and Linux path shapes (the old assertion failed only on Linux), and
requiring the install to run in the caller fails as well.
@aldegad
aldegad merged commit b2cbfae into main Jul 24, 2026
2 checks passed
@aldegad
aldegad deleted the plan/yarn-candidate-closure-materialization branch July 24, 2026 09:13
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.

1 participant