Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions scripts/lib/peer-range.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* The range shape a dependency earns from its own versioning, and npm's rule
* for which versions that shape admits.
*
* From 1.0.0 a package states that a minor is additive, a patch is a fix, and
* only a major removes or narrows, so a caret range holds one installed copy
* across every later minor. A pre-1.0 package states no such promise: npm locks
* a 0.x caret to its minor and a 0.0.z caret to its patch, so the range for a
* pre-1.0 dependency stops at the next minor instead.
*/

function readVersion(version) {
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version)
if (match === null) {
throw new Error(`cannot read version ${version}`)
}
return match.slice(1, 4).map(Number)
}

function compareVersion(left, right) {
for (let index = 0; index < left.length; index += 1) {
const difference = left[index] - right[index]
if (difference !== 0) return difference
}
return 0
}

function exactMinorPeerRange(version) {
const [major, minor] = readVersion(version)
return `>=${version} <${major}.${minor + 1}.0`
}

export function expectedPeerRange(version) {
return readVersion(version)[0] >= 1 ? `^${version}` : exactMinorPeerRange(version)
}

/** The exclusive upper bound of a caret floor, under npm's rule. */
function caretUpperBound([major, minor, patch]) {
if (major > 0) return [major + 1, 0, 0]
if (minor > 0) return [0, minor + 1, 0]
return [0, 0, patch + 1]
}

/**
* A caret range admits an installed version.
*
* The version is compared by its release part, so a prerelease of an admitted
* version counts as admitted: the guards that call this assert one physical
* copy of a contract package, and a prerelease build of that copy speaks the
* same surface.
*/
export function caretAdmits(range, version) {
const caret = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range)
if (caret === null) return false
let installed
try {
installed = readVersion(version)
} catch {
return false
}
const floor = caret.slice(1).map(Number)
return (
compareVersion(installed, floor) >= 0 &&
compareVersion(installed, caretUpperBound(floor)) < 0
)
}
80 changes: 80 additions & 0 deletions scripts/lib/peer-range.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from 'vitest'
import { caretAdmits, expectedPeerRange } from './peer-range.mjs'

// The expected column is npm's own rule, read from semver 7.8.5 with
// includePrerelease. A caret keeps the leftmost non-zero place: ^1.2.3 holds
// major 1, ^0.9.0 holds minor 0.9, and ^0.0.3 holds patch 0.0.3.
describe('caretAdmits', () => {
const table = [
['^1.0.0', '1.0.0', true],
['^1.0.0', '1.4.2', true],
['^1.0.0', '2.0.0', false],
['^1.0.0', '0.9.9', false],
['^1.2.0', '1.1.0', false],
['^1.2.0', '1.2.0', true],
['^1.2.0', '1.99.99', true],
['^0.9.0', '0.9.0', true],
['^0.9.0', '0.9.3', true],
['^0.9.0', '0.10.0', false],
['^0.9.0', '0.8.9', false],
['^0.9.0', '1.0.0', false],
['^0.0.3', '0.0.3', true],
['^0.0.3', '0.0.4', false],
['^0.0.3', '0.0.2', false],
['^0.0.3', '0.1.0', false],
['^0.145.21', '0.145.21', true],
['^0.145.21', '0.145.99', true],
['^0.145.21', '0.146.0', false],
]

for (const [range, version, admitted] of table) {
it(`${range} ${admitted ? 'admits' : 'refuses'} ${version}`, () => {
expect(caretAdmits(range, version)).toBe(admitted)
})
}

// A version is compared by its release part, so a prerelease of an admitted
// release is admitted. This is wider than npm at one point: npm orders
// 1.0.0-rc.1 below 1.0.0 and refuses it under ^1.0.0, and this admits it. The
// callers assert one physical copy of a contract package, and a prerelease
// build of that copy carries the same surface.
it('reads a prerelease by its release part', () => {
expect(caretAdmits('^1.0.0', '1.2.0-develop.1')).toBe(true)
expect(caretAdmits('^0.9.0', '0.9.1-rc.1')).toBe(true)
expect(caretAdmits('^0.9.0', '0.10.0-rc.1')).toBe(false)
expect(caretAdmits('^1.0.0', '1.0.0-rc.1')).toBe(true)
})

it('refuses a range that is not a plain caret', () => {
for (const range of ['>=1.0.0', '1.0.0', '~1.0.0', '*', '^1.0', '']) {
expect(caretAdmits(range, '1.0.0')).toBe(false)
}
})

it('refuses a version it cannot read', () => {
for (const version of ['', '1.0', 'latest', 'v1.0.0']) {
expect(caretAdmits('^1.0.0', version)).toBe(false)
}
})
})

describe('expectedPeerRange', () => {
const table = [
['1.0.0', '^1.0.0'],
['1.4.2', '^1.4.2'],
['8.0.5', '^8.0.5'],
['0.9.0', '>=0.9.0 <0.10.0'],
['0.27.1', '>=0.27.1 <0.28.0'],
['0.145.21', '>=0.145.21 <0.146.0'],
]

for (const [version, range] of table) {
it(`${version} earns ${range}`, () => {
expect(expectedPeerRange(version)).toBe(range)
})
}

it('refuses a version it cannot read', () => {
expect(() => expectedPeerRange('1.0')).toThrow('cannot read version 1.0')
})
})
8 changes: 3 additions & 5 deletions scripts/verify-official-optimizers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import {
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { expectedPeerRange } from './lib/peer-range.mjs'

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const sourcePackage = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'))
const agentEvalVersion = sourcePackage.devDependencies?.['@tangle-network/agent-eval']
if (!/^\d+\.\d+\.\d+$/.test(agentEvalVersion)) {
throw new Error('@tangle-network/agent-eval must have one exact development pin')
}
const [agentEvalMajor, agentEvalMinor] = agentEvalVersion.split('.').map(Number)
const expectedEvalPeerRange = `>=${agentEvalVersion} <${agentEvalMajor}.${agentEvalMinor + 1}.0`
const expectedEvalPeerRange = expectedPeerRange(agentEvalVersion)
if (sourcePackage.peerDependencies?.['@tangle-network/agent-eval'] !== expectedEvalPeerRange) {
throw new Error(
`@tangle-network/agent-eval peer range must be ${expectedEvalPeerRange} to match the development pin`,
Expand All @@ -28,9 +28,7 @@ const agentInterfaceVersion = sourcePackage.devDependencies?.['@tangle-network/a
if (!/^\d+\.\d+\.\d+$/.test(agentInterfaceVersion)) {
throw new Error('@tangle-network/agent-interface must have one exact development pin')
}
// agent-interface states that a minor is additive and only a major removes or
// narrows, so the peer is a caret range on the lowest version this package uses.
const expectedInterfacePeerRange = `^${agentInterfaceVersion}`
const expectedInterfacePeerRange = expectedPeerRange(agentInterfaceVersion)
if (sourcePackage.peerDependencies?.['@tangle-network/agent-interface'] !== expectedInterfacePeerRange) {
throw new Error(
`@tangle-network/agent-interface peer range must be ${expectedInterfacePeerRange} to match the development pin`,
Expand Down
33 changes: 6 additions & 27 deletions scripts/verify-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { caretAdmits, expectedPeerRange } from './lib/peer-range.mjs'

const packageName = '@tangle-network/agent-knowledge'
const publicImports = [
Expand Down Expand Up @@ -66,9 +67,9 @@ const agentInterfacePackage = '@tangle-network/agent-interface'
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const sourcePackage = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'))
const agentEvalVersion = exactDevelopmentPin(sourcePackage, agentEvalPackage)
const agentEvalPeerRange = exactMinorPeerRange(agentEvalVersion)
const agentEvalPeerRange = expectedPeerRange(agentEvalVersion)
const agentInterfaceVersion = exactDevelopmentPin(sourcePackage, agentInterfacePackage)
const agentInterfacePeerRange = caretPeerRange(agentInterfaceVersion)
const agentInterfacePeerRange = expectedPeerRange(agentInterfaceVersion)
assertRequiredPeer(sourcePackage, agentEvalPackage, agentEvalPeerRange)
assertRequiredPeer(sourcePackage, agentInterfacePackage, agentInterfacePeerRange)
assertNoAgentStackOverrides(sourcePackage)
Expand Down Expand Up @@ -277,35 +278,13 @@ function exactDevelopmentPin(packageManifest, packageName) {
return version
}

function exactMinorPeerRange(version) {
const [major, minor] = version.split('.').map(Number)
return `>=${version} <${major}.${minor + 1}.0`
}

// agent-interface states that a minor is additive and only a major removes or
// narrows, so the peer is a caret range on the lowest version this package uses.
function caretPeerRange(version) {
return `^${version}`
}

// A cohort package declares a caret range, not the resolved version, so the two
// are compared by admission: same major, and a floor at or below the version
// that is actually installed.
// are compared by admission under npm's caret rule.
function assertCaretAdmits(declaredRange, version, description) {
const declared = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(declaredRange)
if (declared === null) {
if (!/^\^(\d+)\.(\d+)\.(\d+)$/.test(declaredRange)) {
throw new Error(`${description} must declare a caret range, received ${declaredRange}`)
}
const [declaredMajor, declaredMinor, declaredPatch] = declared.slice(1).map(Number)
const [major, minor, patch] = version.split('.').map(Number)
if (declaredMajor !== major) {
throw new Error(
`${description} declares ${declaredRange}, which does not admit installed ${version}`,
)
}
const declaredFloor = declaredMinor * 1_000_000 + declaredPatch
const installed = minor * 1_000_000 + patch
if (declaredFloor > installed) {
if (!caretAdmits(declaredRange, version)) {
throw new Error(
`${description} declares ${declaredRange}, which does not admit installed ${version}`,
)
Expand Down