From 4f733c49768e3521e412f2dae71a1c9078eaebb1 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 18 Oct 2026 02:19:05 +0800 Subject: [PATCH 1/3] fix(peer): admit the 0.2.x harness line, with -0 floors per tuple The host compares a plugin's peer range against its own runtime version (app-boot's unmetRequirements, reading getDshRuntimeVersion), so once the harness moved to 0.2.1-rc.2 the three 0.1.x branches all capped below 0.2.0 and the install was refused as incompatible. The import this peer guards -- FileSystemSkillProvider -- is unchanged on 0.2.x, and driving it with apply() against a real 0.2.1-rc.2 provider still discovers both skill editions. Each new tuple uses a -0 floor rather than a named prerelease tag: -0 sorts below every prerelease of its tuple, so a later 0.2.2-alpha.1 needs no new branch, and <0.3.0-0 keeps the next minor out until it is actually checked. The range also stays under 256 characters, which the plugin market's discovery cache requires (MAX_RANGE_LENGTH in dshmarket/lib/discovery-compatibility.js); going over it trades a host rejection for a market one. A test now pins both properties, because neither failure surfaces anywhere else in the suite. --- AGENTS.md | 6 ++++++ package.json | 2 +- test/plugin.test.js | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 39e4106..3152ce1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -633,6 +633,12 @@ is faithful โ€” that is a reading job, and a mechanical stand-in would only manu >=0.1.6-0 <0.2.0` is the convention in `dsh-skill-pack-security`. node-semver admits a prerelease only when some comparator shares its tuple *and* itself carries a prerelease tag, so a single `^0.1.5-rc.2` silently excludes `0.1.6-rc.*` and the user meets an `ERESOLVE`. + The 0.2.x tuples are covered with `-0` floors rather than a named tag + (`>=0.2.0-0 <0.2.1-0 || >=0.2.1-0 <0.3.0-0`): `-0` sorts below every prerelease of its + tuple, so a later `0.2.2-alpha.1` needs no new branch, and `<0.3.0-0` keeps the next minor + out until it is actually checked. Keep the whole range under 256 characters โ€” the plugin + market's discovery cache rejects longer ones outright (`MAX_RANGE_LENGTH` in + `dshmarket/lib/discovery-compatibility.js`). - ๐Ÿงช Reproduce the mount without installing a profile: `node .scratch/probe-plugin.mjs` (needs an installed DSH โ€” reach it by junctioning the app's `node_modules` into the repo, which `.gitignore` already covers). It calls `apply()` with a minimal ctx and drives the **real** diff --git a/package.json b/package.json index 37a9a5b..c9f80d1 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ } }, "peerDependencies": { - "@deepseek-ai/dsh-skill-filesystem": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0" + "@deepseek-ai/dsh-skill-filesystem": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0 || >=0.2.0-0 <0.2.1-0 || >=0.2.1-0 <0.3.0-0" }, "peerDependenciesMeta": { "@deepseek-ai/dsh-skill-filesystem": { diff --git a/test/plugin.test.js b/test/plugin.test.js index bd7e8ec..7e3541e 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -74,4 +74,18 @@ describe('the declared peer stays resolvable on prerelease harness builds', () = assert.ok((range.match(/-(rc|alpha|beta)\./g) ?? []).length >= 2, `each covered tuple needs its own prerelease comparator: ${range}`); assert.equal(peerDependenciesMeta?.['@deepseek-ai/dsh-skill-filesystem']?.optional, true, 'the CLI install must not pull the harness in'); }); + + test('the range reaches the 0.2.x harness line and stays readable by the market', async () => { + // Two separate ceilings bit this package on the 0.2.0 release of the harness. + // The host checks the peer range against its OWN version (app-boot's + // unmetRequirements), so a range that stopped at 0.1.x denied the plugin at + // install time; and the plugin market's discovery cache refuses a range over + // MAX_RANGE_LENGTH = 256 characters outright (dshmarket's + // discovery-compatibility.js), so widening past that trades one silent + // rejection for another. Neither failure shows up as a test elsewhere. + const { peerDependencies } = await pkg(); + const range = peerDependencies?.['@deepseek-ai/dsh-skill-filesystem']; + assert.ok(/[><]=\s*0\.2\./.test(range), `the 0.2.x harness line must have its own branch: ${range}`); + assert.ok(range.length < 256, `the market drops ranges this long: ${range.length} chars`); + }); }); From e5e75b1b77cf1c019acc83eee97b22e8757d30f3 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 18 Oct 2026 03:13:17 +0800 Subject: [PATCH 2/3] test(peer): assert admission, not the range's text The two existing assertions on the peer range check its text ("contains 0.2.") and its length (< 256). Both can pass while the range admits nothing: `>=0.2.0-0 <0.2.1-0` is 18 characters, matches the text test, and rejects 0.2.1-rc.2 under both admission rules. The property that has to hold is admission, so assert that directly. Both rules are asserted because the plugin meets both. app-boot's unmetRequirements compares with includePrerelease: true, which is what lets the mount happen at startup; plain `npm install` uses the default rule, and a range that only satisfies the first installs under the host and throws ERESOLVE for the user. This needs semver, and there is no install step in CI (the project is zero-dependency), so test/semver-range.js carries the small slice we use: >= and < comparators joined by ||, semver precedence, and the prerelease admission rule. Anything outside that throws rather than being guessed at, so a future edit reaching for ^ or a hyphen range fails loudly. The helper is pinned against node-semver by frozen cases in test/semver-range.test.js; those expected values were generated from node-semver 7, not written by hand, and should only ever be re-derived the same way. Also corrects the AGENTS.md note this range was documented with. It said a `-0` floor means "a later 0.2.2-alpha.1 needs no new branch". A `-0` floor sorts below every prerelease of *its own* tuple, which is the benefit; it does not reach the next tuple. 0.2.2-alpha.1 is admitted by the host's includePrerelease rule and refused by npm's default rule, so a new tuple still needs its own branch. The wording is now pinned by a test rather than by prose alone. --- AGENTS.md | 14 ++-- test/plugin.test.js | 46 +++++++++++++ test/semver-range.js | 135 ++++++++++++++++++++++++++++++++++++++ test/semver-range.test.js | 98 +++++++++++++++++++++++++++ 4 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 test/semver-range.js create mode 100644 test/semver-range.test.js diff --git a/AGENTS.md b/AGENTS.md index 3152ce1..f6fa388 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -634,10 +634,16 @@ is faithful โ€” that is a reading job, and a mechanical stand-in would only manu prerelease only when some comparator shares its tuple *and* itself carries a prerelease tag, so a single `^0.1.5-rc.2` silently excludes `0.1.6-rc.*` and the user meets an `ERESOLVE`. The 0.2.x tuples are covered with `-0` floors rather than a named tag - (`>=0.2.0-0 <0.2.1-0 || >=0.2.1-0 <0.3.0-0`): `-0` sorts below every prerelease of its - tuple, so a later `0.2.2-alpha.1` needs no new branch, and `<0.3.0-0` keeps the next minor - out until it is actually checked. Keep the whole range under 256 characters โ€” the plugin - market's discovery cache rejects longer ones outright (`MAX_RANGE_LENGTH` in + (`>=0.2.0-0 <0.2.1-0 || >=0.2.1-0 <0.3.0-0`). A `-0` floor sorts below every prerelease + **of its own tuple**, so `>=0.2.1-0` admits `0.2.1-alpha.1` without a named tag โ€” that is + the whole benefit, and it stops at the tuple boundary. It does **not** reach the next + tuple's prereleases: under the default rule `0.2.2-alpha.1` is still refused, because the + comparator that would admit it must share the 0.2.2 tuple. Only the host's + `includePrerelease: true` check admits it, and npm does not use that rule โ€” so shipping a + range on that basis alone means it installs under the host and throws `ERESOLVE` for the + user. Give a new tuple its own `-0` branch. `<0.3.0-0` keeps the next minor out until it + is actually checked. Keep the whole range under 256 characters โ€” the plugin market's + discovery cache rejects longer ones outright (`MAX_RANGE_LENGTH` in `dshmarket/lib/discovery-compatibility.js`). - ๐Ÿงช Reproduce the mount without installing a profile: `node .scratch/probe-plugin.mjs` (needs an installed DSH โ€” reach it by junctioning the app's `node_modules` into the repo, which diff --git a/test/plugin.test.js b/test/plugin.test.js index 7e3541e..a43ecdb 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -14,6 +14,7 @@ import assert from 'node:assert/strict'; import { access, readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { satisfies } from './semver-range.js'; const ROOT = path.resolve(fileURLToPath(new URL('.', import.meta.url)), '..'); const read = (rel) => readFile(path.join(ROOT, rel), 'utf8'); @@ -88,4 +89,49 @@ describe('the declared peer stays resolvable on prerelease harness builds', () = assert.ok(/[><]=\s*0\.2\./.test(range), `the 0.2.x harness line must have its own branch: ${range}`); assert.ok(range.length < 256, `the market drops ranges this long: ${range.length} chars`); }); + + test('the range actually admits the harness versions it claims, not just their text', async () => { + // The text assertions above are necessary and not sufficient. A range can + // carry "0.2." and be short and still reject every 0.2.x prerelease: + // `>=0.2.0-0 <0.2.1-0` passes both checks and refuses 0.2.1-rc.2. The + // property that matters is admission, so it is asserted directly. + // + // Both admission rules are checked because the plugin meets both. The host + // compares with includePrerelease:true (app-boot's unmetRequirements), which + // is how a mount is allowed at startup; plain `npm install` uses the default + // rule, which is how the install either resolves or throws ERESOLVE. A + // range that satisfies only the first is a package that installs nowhere. + const { peerDependencies } = await pkg(); + const range = peerDependencies?.['@deepseek-ai/dsh-skill-filesystem']; + for (const version of ['0.1.6-rc.0', '0.1.7-rc.1', '0.2.0-rc.1', '0.2.1-rc.1', '0.2.1-rc.2', '0.2.1', '0.2.5']) { + assert.ok(satisfies(version, range, { includePrerelease: true }), + `the harness at ${version} must be admitted: ${range}`); + } + // The default rule has to admit the plain releases and the prereleases whose + // tuple has its own branch. This is the rule `npm install` applies. + for (const version of ['0.1.6-0', '0.1.7-0', '0.2.0-0', '0.2.1-0', '0.2.1-rc.2', '0.2.1']) { + assert.ok(satisfies(version, range, { includePrerelease: false }), + `npm's own rule must admit ${version}, or the install fails with ERESOLVE: ${range}`); + } + for (const version of ['0.0.9', '0.1.1', '0.3.0-0', '0.3.0', '1.0.0']) { + assert.ok(!satisfies(version, range, { includePrerelease: true }), + `${version} was never checked and must stay out: ${range}`); + } + }); + + test('a -0 floor covers its own tuple only, and the suite states that boundary', async () => { + // Regression lock on the wording in AGENTS.md. `-0` sorts below every + // prerelease of ITS tuple, so `>=0.2.1-0 <0.3.0-0` admits 0.2.1-alpha.1 + // without a new branch. It does NOT reach the next tuple's prereleases: the + // host's includePrerelease rule admits 0.2.2-alpha.1, npm's default rule + // does not. Writing the second half as if it also held is what an earlier + // draft of the AGENTS.md note did, so the true boundary is pinned here. + const branch = '>=0.2.1-0 <0.3.0-0'; + for (const version of ['0.2.1-0', '0.2.1-alpha.1', '0.2.1-rc.1', '0.2.1', '0.2.2']) { + assert.ok(satisfies(version, branch, { includePrerelease: false }), `${version} shares the tuple and must be admitted`); + } + assert.ok(satisfies('0.2.2-alpha.1', branch, { includePrerelease: true }), 'the host rule reaches the next tuple'); + assert.ok(!satisfies('0.2.2-alpha.1', branch, { includePrerelease: false }), + 'the npm rule does not: a later tuple needs its own branch'); + }); }); diff --git a/test/semver-range.js b/test/semver-range.js new file mode 100644 index 0000000..681582f --- /dev/null +++ b/test/semver-range.js @@ -0,0 +1,135 @@ +/** + * Enough of node-semver to ask one question about one range: "is this version + * admitted?" โ€” used to test the peer range we publish. + * + * Why not just import `semver`: this repository is deliberately zero-dependency + * and CI has no install step (.github/workflows/ci.yml), so a devDependency + * would not resolve on a runner. AGENTS.md allows a third-party dependency only + * after auditing its source; that is a poor trade for a test helper. + * + * Why not a regex over the range text: the text is not the property under test. + * `>=0.2.0-0 <0.2.1-0` contains "0.2." and is short, yet it rejects + * 0.2.1-rc.2 โ€” that is exactly the class of bug this file exists to catch. + * + * Scope is deliberately narrow: it understands only `>=` and `<` comparators, + * joined by whitespace, in branches joined by `||`. Anything else throws. The + * published range is written in exactly those forms, so a future edit that + * reaches for `^`, `~` or a hyphen range fails here loudly instead of being + * quietly mis-parsed into a passing test. + * + * This is NOT a semver implementation for production use. It is verified + * against the real node-semver over a version matrix in + * test/plugin.test.js โ€” if it ever drifts, that comparison fails first. + */ + +const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/; +const COMPARATOR_RE = /^(>=|<)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/; + +/** Parse `major.minor.patch[-prerelease]`. Build metadata is out of scope. */ +export function parseVersion(text) { + const m = VERSION_RE.exec(String(text).trim()); + if (!m) throw new Error(`not a version this helper understands: ${text}`); + return { + major: Number(m[1]), + minor: Number(m[2]), + patch: Number(m[3]), + prerelease: m[4] ? m[4].split('.') : [] + }; +} + +const numeric = (id) => /^\d+$/.test(id); + +/** + * Semver precedence (https://semver.org/#spec-item-11). Returns -1, 0 or 1. + * + * A version with a prerelease sorts below the same tuple without one; within a + * prerelease, numeric identifiers compare numerically and sort below + * alphanumeric ones, and a shorter identifier list sorts below a longer one + * once every shared position is equal. + */ +export function compareVersions(a, b) { + for (const key of ['major', 'minor', 'patch']) { + if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1; + } + const ap = a.prerelease; + const bp = b.prerelease; + if (ap.length === 0 || bp.length === 0) { + if (ap.length === bp.length) return 0; + // No prerelease outranks any prerelease. + return ap.length === 0 ? 1 : -1; + } + for (let i = 0; i < Math.min(ap.length, bp.length); i++) { + const x = ap[i]; + const y = bp[i]; + if (x === y) continue; + if (numeric(x) && numeric(y)) return Number(x) < Number(y) ? -1 : 1; + if (numeric(x)) return -1; + if (numeric(y)) return 1; + return x < y ? -1 : 1; + } + if (ap.length === bp.length) return 0; + return ap.length < bp.length ? -1 : 1; +} + +/** Split a range into branches of parsed comparators. Throws on unknown syntax. */ +export function parseRange(range) { + return String(range) + .split('||') + .map((branch) => { + const parts = branch.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) throw new Error(`empty range branch in: ${range}`); + return parts.map((text) => { + const m = COMPARATOR_RE.exec(text); + if (!m) throw new Error(`comparator "${text}" is outside this helper's scope (only >= and <): ${range}`); + return { op: m[1], version: parseVersion(m[2]) }; + }); + }); +} + +/** + * Mirrors node-semver's `satisfies(version, range, { includePrerelease })`. + * + * The prerelease rule is the subtle half and the reason this helper exists. By + * default node-semver refuses a prerelease version unless some comparator *in + * the same branch* both shares its exact major.minor.patch tuple *and* itself + * carries a prerelease tag. So `>=0.1.6-0 <0.2.0` cannot admit `0.2.1-rc.2` no + * matter how the numbers read. `includePrerelease: true` drops that requirement + * and compares on precedence alone โ€” which is what the harness does + * (app-boot's unmetRequirements calls semver.satisfies with includePrerelease), + * while plain `npm install` uses the default. Both are checked in the tests + * because the plugin has to survive both. + */ +export function satisfies(version, range, { includePrerelease = false } = {}) { + const v = parseVersion(version); + const branches = parseRange(range); + // node-semver tests each `||` branch on its own and returns true as soon as + // one passes โ€” including the prerelease rule, which is evaluated per branch. + // Short-circuiting on the first branch whose comparators match is wrong: a + // version can match a wide branch numerically and still be refused there + // because that branch's prerelease comparator names a different tuple, while + // a later branch admits it legitimately. + return branches.some((candidates) => { + const inRange = candidates.every((c) => { + const order = compareVersions(v, c.version); + return c.op === '>=' ? order >= 0 : order < 0; + }); + if (!inRange) return false; + if (includePrerelease || v.prerelease.length === 0) return true; + return candidates.some((c) => c.version.prerelease.length > 0 + && c.version.major === v.major + && c.version.minor === v.minor + && c.version.patch === v.patch); + }); +} + +/** + * A table of [version, expected] pairs checked against one range under one + * admission rule. Used by the peer-range tests so a failure names the version + * and the rule rather than just "the range is wrong". + */ +export function partition(versions, range, options) { + const admitted = []; + const rejected = []; + for (const version of versions) (satisfies(version, range, options) ? admitted : rejected).push(version); + return { admitted, rejected }; +} diff --git a/test/semver-range.test.js b/test/semver-range.test.js new file mode 100644 index 0000000..e51c3ca --- /dev/null +++ b/test/semver-range.test.js @@ -0,0 +1,98 @@ +/** + * Locks the zero-dependency range helper against node-semver. + * + * test/semver-range.js exists because this repository has no install step in CI + * (see .github/workflows/ci.yml) and AGENTS.md forbids a casual third-party + * dependency โ€” so the suite cannot import the real node-semver at all. But a + * hand-written comparator is exactly the kind of thing that is subtly wrong + * while looking right, and a wrong helper makes every peer-range assertion pass + * for the wrong reason. + * + * The cases below were generated from node-semver 7 (semver.satisfies with and + * without includePrerelease, semver.compare) and frozen. They are the tricky + * ones: numeric vs alphanumeric identifiers, shorter identifier lists, a + * prerelease sorting below its own release, and per-branch evaluation of `||`. + * If the helper drifts, this fails here rather than silently mis-answering in + * test/plugin.test.js. + * + * Do not "fix" a failure by editing the expected values. Re-derive them from + * node-semver and say so in the commit message. + */ +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { compareVersions, parseVersion, satisfies, parseRange } from './semver-range.js'; + +// [a, b, expected sign of compare(a, b)] โ€” from semver.compare +const PRECEDENCE = [ + ['0.2.0-0', '0.2.0', -1], + ['0.2.0-rc.1', '0.2.0', -1], + ['0.2.0-alpha.1', '0.2.0-rc.1', -1], + ['0.1.6-rc.9', '0.1.6-rc.10', -1], + ['0.1.6-rc.10', '0.1.6-rc.9', 1], + ['0.1.6-1', '0.1.6-alpha', -1], + ['0.1.6-alpha', '0.1.6-1', 1], + ['0.1.6-alpha.1', '0.1.6-alpha', 1], + ['0.1.6-alpha', '0.1.6-alpha.1', -1], + ['0.1.9', '0.2.0-0', -1], + ['0.2.1-0', '0.2.1-rc.1', -1], + ['0.2.1-rc.2', '0.2.1', -1], + ['1.0.0-0', '0.9.9', 1], + ['0.1.6-x.1', '0.1.6-1', 1], + ['0.1.6-beta.2', '0.1.6-beta.10', -1] +]; + +// [version, range, includePrerelease, expected] โ€” from semver.satisfies +const SATISFIES = [ + ['0.2.1-rc.2', '>=0.2.1-0 <0.3.0-0', true, true], + ['0.2.1-rc.2', '>=0.2.1-0 <0.3.0-0', false, true], + ['0.2.2-alpha.1', '>=0.2.1-0 <0.3.0-0', true, true], + ['0.2.2-alpha.1', '>=0.2.1-0 <0.3.0-0', false, false], + ['0.2.1-rc.2', '>=0.2.0-0 <0.2.1-0', true, false], + ['0.1.6-rc.1', '>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0', true, true], + ['0.1.6-rc.1', '>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0', false, true], + ['0.1.9', '>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0', false, true], + ['0.2.0-rc.1', '>=0.1.6-0 <0.2.0 || >=0.2.0-0 <0.3.0-0', false, true], + ['0.2.0-rc.1', '>=0.1.6-0 <0.2.0 || >=0.2.0-0 <0.3.0-0', true, true], + ['0.3.0-0', '>=0.1.6-0 <0.2.0 || >=0.2.0-0 <0.3.0-0', true, false], + ['0.1.5-rc.2', '>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0', false, true] +]; + +describe('the range helper agrees with node-semver on the cases that matter', () => { + test('precedence matches semver.compare', () => { + for (const [a, b, expected] of PRECEDENCE) { + const got = Math.sign(compareVersions(parseVersion(a), parseVersion(b))); + assert.equal(got, expected, `compare(${a}, ${b})`); + // and the reverse direction has to be consistent, not independently lucky + assert.equal(Math.sign(compareVersions(parseVersion(b), parseVersion(a))), -expected, `compare(${b}, ${a})`); + } + }); + + test('satisfies matches semver.satisfies under both admission rules', () => { + for (const [version, range, includePrerelease, expected] of SATISFIES) { + assert.equal(satisfies(version, range, { includePrerelease }), expected, + `satisfies(${version}, ${range}, {includePrerelease:${includePrerelease}})`); + } + }); + + test('branches are evaluated independently, so a wide first branch cannot mask a later one', () => { + // This is the bug the first draft of the helper had, and it is invisible in + // any test whose range has a single branch. `>=0.1.2-rc.1 <0.2.0` matches + // 0.1.6-rc.1 numerically, but under the default rule that branch refuses it + // (its prerelease comparator names the 0.1.2 tuple); the third branch is + // what admits it. Short-circuiting on the first numeric match returns false. + const range = '>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0'; + assert.equal(satisfies('0.1.6-rc.1', range, { includePrerelease: false }), true); + assert.equal(parseRange(range).length, 3); + }); + + test('a comparator this helper cannot read fails loudly instead of being guessed at', () => { + // The published range uses only >= and <. If a future edit reaches for a + // caret or a hyphen range, a silent mis-parse would make the peer tests + // meaningless, so the helper throws and the suite stops. + for (const unsupported of ['^0.1.6', '~0.2.0', '0.1.6 - 0.2.0', '>=0.1.6 <0.2.0 || ']) { + assert.throws(() => parseRange(unsupported), /outside this helper's scope|empty range branch/, + `${unsupported} must not be silently accepted`); + } + assert.throws(() => parseVersion('0.1'), /not a version this helper understands/); + }); +}); From 9648223029a02a1d909a9e5e11f56ccca8d2d386 Mon Sep 17 00:00:00 2001 From: modusensus Date: Sun, 18 Oct 2026 03:18:22 +0800 Subject: [PATCH 3/3] docs(agents): the market also passes includePrerelease, so name both callers The host's unmetRequirements is not the only caller that widens prerelease admission: dshmarket's rangeResult passes includePrerelease: true too (lib/discovery-compatibility.js:74). Naming only the host made the boundary look narrower than it is, and the point of the sentence is which rule npm uses, not who else agrees with the host. --- AGENTS.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f6fa388..c3d0105 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -638,11 +638,12 @@ is faithful โ€” that is a reading job, and a mechanical stand-in would only manu **of its own tuple**, so `>=0.2.1-0` admits `0.2.1-alpha.1` without a named tag โ€” that is the whole benefit, and it stops at the tuple boundary. It does **not** reach the next tuple's prereleases: under the default rule `0.2.2-alpha.1` is still refused, because the - comparator that would admit it must share the 0.2.2 tuple. Only the host's - `includePrerelease: true` check admits it, and npm does not use that rule โ€” so shipping a - range on that basis alone means it installs under the host and throws `ERESOLVE` for the - user. Give a new tuple its own `-0` branch. `<0.3.0-0` keeps the next minor out until it - is actually checked. Keep the whole range under 256 characters โ€” the plugin market's + comparator that would admit it must share the 0.2.2 tuple. A check that passes + `includePrerelease: true` โ€” the host's `unmetRequirements`, the market's range check โ€” admits it; + npm's default rule, the one `npm install` actually uses, does not, and a range shipped on that + basis alone means it installs under the host and throws `ERESOLVE` for the user. Give a new tuple + its own `-0` branch. `<0.3.0-0` keeps the next minor out until it is + actually checked. Keep the whole range under 256 characters โ€” the plugin market's discovery cache rejects longer ones outright (`MAX_RANGE_LENGTH` in `dshmarket/lib/discovery-compatibility.js`). - ๐Ÿงช Reproduce the mount without installing a profile: `node .scratch/probe-plugin.mjs` (needs