From a433360992d0da148a9c9e6ef9928732090d704a Mon Sep 17 00:00:00 2001 From: paulohenriquevn Date: Mon, 7 Sep 2026 09:54:42 -0300 Subject: [PATCH] fix(dep-check): a workspace: range is not a moved contract (0.11.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.11.0 compared the workspace manifest against the published one key by key, and `pnpm pack` rewrites `workspace:^` to the version it resolves to. The two therefore differ on that key in EVERY package that depends on a sibling, so `contractMoved` was true almost everywhere — handing a local tarball to nearly every sibling and undoing the exact narrowness the comparison exists to keep. Measured on the usetheokit/theokit workspace: three packages substituted with no contract having moved, each one reported with a reason that was not true of it. The pre-existing test protecting the narrowness passed throughout, because it uses literal ranges and never a workspace protocol — which is why a test for this case is added rather than an assertion tightened. A local protocol (workspace:, link:, file:, portal:) is a placeholder rather than a contract, and whether the version it resolves to is published is already the question the unpublished branch answers. Those keys are skipped. Re-proved on the real checkout, both directions: #659 condition on disk only @theokit/presenter substituted, reason correct condition removed no substitution at all 0.11.0 is published and should not be used. The defect makes the check weaker, never falsely blocking, but the pin moves to 0.11.1 in the same commit so no caller stays on it. --- .github/workflows/dep-check.yml | 2 +- CHANGELOG.md | 18 ++++++++++++++++++ packages/dep-check/package.json | 2 +- packages/dep-check/src/checks.mjs | 11 ++++++++++- packages/dep-check/test/checks.test.mjs | 21 +++++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dep-check.yml b/.github/workflows/dep-check.yml index 87f48ba..eec5ed5 100644 --- a/.github/workflows/dep-check.yml +++ b/.github/workflows/dep-check.yml @@ -32,7 +32,7 @@ on: TOOL is a semver artifact, so a behaviour change is a version bump somebody reviewed. The pin lives here rather than in eleven callers. type: string - default: '0.11.0' + default: '0.11.1' run-floor-check: description: | Also run the suite against the BOTTOM of every declared sibling range, not only diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2a0ed..9778996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + +## [0.11.1] - 2026-09-07 + +### Fixed + +- **A `workspace:` range no longer reads as a moved contract (usetheokit/theokit#659).** 0.11.0 + compared the workspace manifest against the published one key by key, and `pnpm pack` rewrites + `workspace:^` to the version it resolves to — so the two differ on that key in EVERY package that + depends on a sibling. `contractMoved` was therefore true almost everywhere, handing a local tarball + to nearly every sibling and undoing the narrowness the comparison exists to keep. Measured on the + `usetheokit/theokit` workspace, where three packages were substituted with no contract having moved. + + A local protocol (`workspace:`, `link:`, `file:`, `portal:`) is a placeholder rather than a + contract, and whether the version it resolves to is published is already the question the + unpublished branch answers. Those keys are skipped. + + 0.11.0 is published and should not be used: its substitution is broad where it was meant to be + narrow. The failure it causes is a weaker check, never a false block. ## [0.11.0] - 2026-09-07 ### Fixed diff --git a/packages/dep-check/package.json b/packages/dep-check/package.json index 9a1d42e..49764af 100644 --- a/packages/dep-check/package.json +++ b/packages/dep-check/package.json @@ -1,6 +1,6 @@ { "name": "@theokit/dep-check", - "version": "0.11.0", + "version": "0.11.1", "description": "The ecosystem dependency gate: does a package's declared range still describe the sibling it ships against? Four checks, kept apart by what they need to answer and therefore by whether they may fail a build.", "type": "module", "engines": { diff --git a/packages/dep-check/src/checks.mjs b/packages/dep-check/src/checks.mjs index b0d0499..b9000ff 100644 --- a/packages/dep-check/src/checks.mjs +++ b/packages/dep-check/src/checks.mjs @@ -333,7 +333,16 @@ function contractMoved(local, publishedManifest) { function sameRanges(a = {}, b = {}) { const keys = new Set([...Object.keys(a), ...Object.keys(b)]); - for (const k of keys) if (a[k] !== b[k]) return false; + for (const k of keys) { + // A local protocol is a PLACEHOLDER, not a contract. `pnpm pack` rewrites `workspace:^` to the + // version it resolves to, so the workspace manifest and the published one differ on that key in + // every package that depends on a sibling. Comparing them raw made `contractMoved` true almost + // everywhere, which hands a local tarball to almost everything and undoes the narrowness this + // comparison exists to keep. Whether the resolved version is published is already the question + // the branch above answers. + if (LOCAL_PROTOCOL.test(a[k] ?? "") || LOCAL_PROTOCOL.test(b[k] ?? "")) continue; + if (a[k] !== b[k]) return false; + } return true; } diff --git a/packages/dep-check/test/checks.test.mjs b/packages/dep-check/test/checks.test.mjs index 5aee5ca..202c79c 100644 --- a/packages/dep-check/test/checks.test.mjs +++ b/packages/dep-check/test/checks.test.mjs @@ -287,6 +287,27 @@ describe("unpublishedSiblings — what check D cannot get from the registry yet" expect(out[0].reason).toBe("contract-moved"); }); + it("test_a_workspace_protocol_range_is_not_a_moved_contract", () => { + // The workspace manifest says `workspace:^` and the published one says the version pnpm + // rewrote it to at pack time. Comparing those two raw strings finds a difference in EVERY + // package that depends on a sibling, which would hand a local tarball to almost everything and + // undo the narrowness the test above protects. A local protocol is a placeholder, not a + // contract — the version it resolves to is already the `unpublished` branch's job. + const ws = [ + { + name: "@theokit/agents", + version: "13.0.0", + dir: "/w/packages/agents", + manifest: { dependencies: { "@theokit/presenter": "workspace:^", "@theokit/sdk": "^5.0.0" } }, + }, + ]; + const pub = { "@theokit/agents": ["13.0.0"] }; + const manifests = { + "@theokit/agents": { "13.0.0": { dependencies: { "@theokit/presenter": "0.9.0", "@theokit/sdk": "^5.0.0" } } }, + }; + expect(unpublishedSiblings({ references: [{ dep: "@theokit/agents" }], workspace: ws, published: pub, manifests })).toEqual([]); + }); + it("test_leaves_a_published_version_alone_when_the_local_contract_is_identical", () => { // The narrowness is the point (#659). Substituting whenever a package is in the workspace would // stop testing what a consumer resolves — the reason the skip exists at all. Only a contract