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