From 4d926a749d0fd0c3f63a3378857d9e7eb5654262 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 08:59:00 +0000 Subject: [PATCH] feat(cloud-connection,metadata-core): an install the protocol handshake admitted unchecked says so in its answer POST /api/v1/marketplace/install-local answers data.protocolCheckSkipped ({ reason, packageId, protocolVersion, message }, plus requiredRange and rangeSource for unparsed-range) when the ADR-0087 handshake admitted the package without checking it, on the managed, unmanaged and paired customization success answers; omitted when the range was checked. The reason is the handshake's own verdict: assertProtocolCompat now returns the verdict it admitted the package under, and the no-range / unparsed-range verdicts carry the protocolVersion they were judged against. The warn log line stays. Claude-Session: https://claude.ai/code/session_01Utp6CZ35sjTKgyrvdkytCz Co-authored-by: Claude --- ...6-install-answer-protocol-check-skipped.md | 12 +++ ...l-local-customization-pair-install.test.ts | 19 +++++ .../marketplace-install-local-jobs.test.ts | 9 ++- .../src/marketplace-install-local-plugin.ts | 80 ++++++++++++++++++- ...e-install-local-protocol-handshake.test.ts | 77 +++++++++++++++++- .../src/protocol-handshake.test.ts | 21 +++++ .../metadata-core/src/protocol-handshake.ts | 38 ++++++--- 7 files changed, 242 insertions(+), 14 deletions(-) create mode 100644 .changeset/30326-install-answer-protocol-check-skipped.md diff --git a/.changeset/30326-install-answer-protocol-check-skipped.md b/.changeset/30326-install-answer-protocol-check-skipped.md new file mode 100644 index 00000000000..3b209cf295a --- /dev/null +++ b/.changeset/30326-install-answer-protocol-check-skipped.md @@ -0,0 +1,12 @@ +--- +'@objectstack/cloud-connection': patch +'@objectstack/metadata-core': patch +--- + +feat(cloud-connection,metadata-core): an install the protocol handshake admitted without checking says so in its answer, not only in the server log + +Clause-②: yes (widening) +Metadata-protocol: none + +- **`@objectstack/cloud-connection`: `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped`** when the ADR-0087 handshake admitted the package without checking it: `{ reason, packageId, protocolVersion, message }`, plus `requiredRange` and `rangeSource` for `reason: 'unparsed-range'`. `reason` is the handshake's own verdict: `no-range` (the package declares no `engines.protocol`) or `unparsed-range` (it declares a range the handshake cannot read). `protocolVersion` is the protocol the package was installed under unchecked, and `message` says so in plain words for the person who pressed Install. It is on all three success answers (managed, `unmanaged`, and a paired customization package), and omitted, not nulled, when the range was checked, so that answer's key set is unchanged. The `warn` log line stays as it was. +- **`@objectstack/metadata-core`: `assertProtocolCompat` returns the verdict it admitted the package under** (new exported type `ProtocolCompatAdmitted`; it returned `void`), and the `no-range` and `unparsed-range` verdicts of `checkProtocolCompat` carry `protocolVersion`, the version they were judged against. A door that answers a caller reads the skipped check from the handshake itself instead of judging the range a second time. diff --git a/packages/cloud-connection/src/marketplace-install-local-customization-pair-install.test.ts b/packages/cloud-connection/src/marketplace-install-local-customization-pair-install.test.ts index 0b22c0a39db..0bd325ef483 100644 --- a/packages/cloud-connection/src/marketplace-install-local-customization-pair-install.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-customization-pair-install.test.ts @@ -27,6 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; +import { PROTOCOL_MAJOR, PROTOCOL_VERSION } from '@objectstack/spec/kernel'; // The runtime binder the door's step 1d loads lazily — paid here, at module // top, never inside a clocked `it` (`scripts/check-test-source-alias.mjs`). import '@objectstack/runtime'; @@ -368,3 +369,21 @@ describe('install-local — a pair install refused before any write', () => { expectNoCodeInstall(h); }); }); + +describe('[#30326] a pair install the handshake admitted unchecked says so in its answer', () => { + it('a pair export with no engines.protocol answers `protocolCheckSkipped` reason `no-range`', async () => { + const h = await mount(dir); + const res = await h.install(exportedPair()); + expect(res.status, JSON.stringify(res.payload)).toBe(200); + const { message, ...machine } = res.payload.data.protocolCheckSkipped; + expect(machine).toEqual({ reason: 'no-range', packageId: PAIR, protocolVersion: PROTOCOL_VERSION }); + expect(message).toContain(`'${PAIR}'`); + }); + + it(`control: a pair export that declares ^${PROTOCOL_MAJOR} answers no \`protocolCheckSkipped\` key`, async () => { + const h = await mount(dir); + const res = await h.install(exportedPair({ engines: { protocol: `^${PROTOCOL_MAJOR}` } })); + expect(res.status, JSON.stringify(res.payload)).toBe(200); + expect(Object.keys(res.payload.data)).not.toContain('protocolCheckSkipped'); + }); +}); diff --git a/packages/cloud-connection/src/marketplace-install-local-jobs.test.ts b/packages/cloud-connection/src/marketplace-install-local-jobs.test.ts index 9559db8f6b1..8050f6f238c 100644 --- a/packages/cloud-connection/src/marketplace-install-local-jobs.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-jobs.test.ts @@ -47,6 +47,7 @@ import { tmpdir } from 'node:os'; // the plugin reaches the same module through a dynamic `import()`. import '@objectstack/runtime'; import { SCHEDULED_WORK_ENV } from '@objectstack/types'; +import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel'; import { MarketplaceInstallLocalPlugin } from './marketplace-install-local-plugin.js'; import { installerAuthService, withInstallerGrants } from './install-local-principal.fixtures.js'; import { LocalManifestSource } from './local-manifest-source.js'; @@ -66,7 +67,13 @@ const HANDLER_JOB = { name: 'jobs_app_tick_handler', schedule: INTERVAL, handler /** The compiled-artifact shape `os build` writes and `os package install` sends. */ function artifact(jobs: unknown[] | undefined, id: string = APP_ID) { return { - manifest: { id, namespace: 'jobs_app', version: '0.1.0', type: 'app', name: 'Jobs App' }, + // A range this runtime admits, as a scaffolded app declares one: the + // answer then carries no `protocolCheckSkipped` notice, and the key + // set the jobs-free case pins is the jobs gate's alone. + manifest: { + id, namespace: 'jobs_app', version: '0.1.0', type: 'app', name: 'Jobs App', + engines: { protocol: `^${PROTOCOL_MAJOR}` }, + }, objects: [{ name: TICK, label: 'Tick', fields: { name: { type: 'text', label: 'Name' } } }], ...(jobs ? { jobs } : {}), }; diff --git a/packages/cloud-connection/src/marketplace-install-local-plugin.ts b/packages/cloud-connection/src/marketplace-install-local-plugin.ts index 21ac39146e1..8e7fdb92f5d 100644 --- a/packages/cloud-connection/src/marketplace-install-local-plugin.ts +++ b/packages/cloud-connection/src/marketplace-install-local-plugin.ts @@ -207,6 +207,7 @@ import { PackageExportManifestSchema, type PackageExportManifest } from '@object import { assertProtocolCompat, checkProtocolCompat, + type ProtocolCompatAdmitted, // [ADR-0141 D2] The pre-install dependency check: the one predicate and // the one answer every package-install door gives its refusal. checkDeclaredDependencies, @@ -728,6 +729,58 @@ function withheldMapsLogLine( + 'environment variables until each map is supplied again where the connector is authored.'; } +/** + * [#30326] What an install answer carries when ADR-0087 D1's handshake + * admitted the package WITHOUT checking it: the person who pressed Install is + * the one who needs to know, and the `warn` line alone reaches only whoever + * reads the server log. Present on a success answer only then; omitted, not + * nulled, for a package whose range was checked, so that answer's key set is + * unchanged. + * + * `reason` is the handshake's own verdict, never a second reading of the + * range: `no-range` (the package declares no `engines.protocol`, nor either + * legacy spelling) or `unparsed-range` (it declares a range the handshake's + * grammar does not read). `protocolVersion` is the protocol the package was + * loaded under unchecked. + */ +export type ProtocolCheckSkipped = + | { reason: 'no-range'; packageId: string; protocolVersion: string; message: string } + | { + reason: 'unparsed-range'; + packageId: string; + protocolVersion: string; + requiredRange: string; + rangeSource: string; + message: string; + }; + +/** [#30326] The notice for a verdict the handshake admitted unchecked; `undefined` for a checked one. */ +function protocolCheckSkippedOf(verdict: ProtocolCompatAdmitted, packageId: string): ProtocolCheckSkipped | undefined { + if (verdict.status === 'ok') return undefined; + const consequence = `so it was installed under platform protocol ${verdict.protocolVersion} without a compatibility ` + + 'check and may not work on this platform version. A build of the package that declares the protocol it ' + + 'supports (`engines.protocol`) is checked at install.'; + switch (verdict.status) { + case 'no-range': + return { + reason: 'no-range', + packageId, + protocolVersion: verdict.protocolVersion, + message: `Package '${packageId}' does not state which platform protocol it supports, ${consequence}`, + }; + case 'unparsed-range': + return { + reason: 'unparsed-range', + packageId, + protocolVersion: verdict.protocolVersion, + requiredRange: verdict.requiredRange, + rangeSource: verdict.source, + message: `Package '${packageId}' states its platform protocol as '${verdict.requiredRange}' ` + + `(${verdict.source}), which this runtime cannot read, ${consequence}`, + }; + } +} + /** * [ADR-0131 D6] The package manifest an install body DECLARES — where each of * the two single-package shapes this door accepts carries it: a compiled @@ -1911,8 +1964,14 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // hooks with THIS runtime's binder: ADR-0087 D1 checks "before // loading a package's metadata". An absent or unrecognised range is // admitted with a warning, as at every seam the handshake guards. + // + // [#30326] The log line is the operator's; the verdict it admitted + // the package under is also the answer's: every success answer + // below carries `protocolCheckSkipped` when the check did not run. + let protocolCheckSkipped: ProtocolCheckSkipped | undefined; try { - assertProtocolCompat(manifest, undefined, (m) => ctx.logger?.warn?.(`[MarketplaceInstallLocal] ${m}`)); + const admitted = assertProtocolCompat(manifest, undefined, (m) => ctx.logger?.warn?.(`[MarketplaceInstallLocal] ${m}`)); + protocolCheckSkipped = protocolCheckSkippedOf(admitted, manifestId); } catch (err) { if (!isProtocolIncompatibleError(err)) throw err; const refusal = protocolIncompatibleAnswer(err); @@ -2090,6 +2149,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin { versionId: resolvedVersionId, userId, withheldMaps, + protocolCheckSkipped, }); } @@ -2135,6 +2195,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin { versionId: resolvedVersionId, userId, withheldMaps, + protocolCheckSkipped, }); } @@ -2520,6 +2581,11 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // withheld whole, so it arrived absent. Omitted when there are // none, so that answer's key set is unchanged. ...(withheldMaps.length > 0 ? { withheldMaps } : {}), + // [#30326] The handshake admitted the package without checking + // it (no `engines.protocol`, or a range it cannot read): the + // installer is told, not only the server log. Omitted when the + // range was checked, so that answer's key set is unchanged. + ...(protocolCheckSkipped !== undefined ? { protocolCheckSkipped } : {}), upgradedFrom: conflict === 'marketplace' ? 'previous-marketplace-version' : null, // [ADR-0146 D2, #28410] Names installed dependents already held, // undeclared, that this version now ships (step 2d). Omitted when @@ -3738,9 +3804,11 @@ export class MarketplaceInstallLocalPlugin implements Plugin { manifest: Record; manifestId: string; version: string; versionId: string; userId: string | null; /** [#28640 ruling ③A] The connector maps the export withheld ({@link judgeWithheldMaps}). */ withheldMaps: readonly WithheldMap[]; + /** [#30326] The handshake admitted the package unchecked ({@link protocolCheckSkippedOf}). */ + protocolCheckSkipped: ProtocolCheckSkipped | undefined; }, ): Promise => { - const { manifest, manifestId, version, versionId, userId, withheldMaps } = args; + const { manifest, manifestId, version, versionId, userId, withheldMaps, protocolCheckSkipped } = args; let protocol: UnmanagedImporter | undefined; try { protocol = ctx.getService('protocol'); } catch { /* no protocol service */ } if (!protocol || typeof protocol.importUnmanagedPackage !== 'function') { @@ -3820,6 +3888,8 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // withheld whole, so its copy carries it absent. Omitted when // there are none, so that answer's key set is unchanged. ...(withheldMaps.length > 0 ? { withheldMaps: [...withheldMaps] } : {}), + // [#30326] Omitted when the handshake checked the package's range. + ...(protocolCheckSkipped !== undefined ? { protocolCheckSkipped } : {}), ...(declaresSeeds ? { seeded: { @@ -3891,9 +3961,11 @@ export class MarketplaceInstallLocalPlugin implements Plugin { version: string; versionId: string; userId: string | null; /** [#28640 ruling ③A] The connector maps the export withheld ({@link judgeWithheldMaps}). */ withheldMaps: readonly WithheldMap[]; + /** [#30326] The handshake admitted the package unchecked ({@link protocolCheckSkippedOf}). */ + protocolCheckSkipped: ProtocolCheckSkipped | undefined; }, ): Promise => { - const { manifest, manifestId, baseId, version, versionId, userId, withheldMaps } = args; + const { manifest, manifestId, baseId, version, versionId, userId, withheldMaps, protocolCheckSkipped } = args; const refuse = (status: number, code: string, message: string, details?: Record) => c.json({ success: false, error: { code, message, ...(details ? { details } : {}) } }, status as any); @@ -4054,6 +4126,8 @@ export class MarketplaceInstallLocalPlugin implements Plugin { written, // [#28640 ruling ③A] Omitted when the export withheld none. ...(withheldMaps.length > 0 ? { withheldMaps: [...withheldMaps] } : {}), + // [#30326] Omitted when the handshake checked the package's range. + ...(protocolCheckSkipped !== undefined ? { protocolCheckSkipped } : {}), ...(plan.carriesSeedData ? { seeded: { diff --git a/packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts b/packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts index 72637978493..c4faf30038f 100644 --- a/packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts @@ -96,17 +96,28 @@ function makeRawApp() { * hand back every writer the install path can reach, so a refusal can be * checked for having written nothing at all. */ -async function mount(dir: string, opts: { controlPlaneUrl?: string } = {}) { +async function mount(dir: string, opts: { controlPlaneUrl?: string; unmanagedImport?: boolean } = {}) { const rawApp = makeRawApp(); const hooks = new Map(); const registry: any[] = []; const register = vi.fn((m: any) => { registry.push({ manifest: m }); }); const syncSchemas = vi.fn(async () => undefined); const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + // [#30326] The protocol's copy-once import, for an `unmanaged` install. + const importUnmanagedPackage = vi.fn(async (request: any) => ({ + sourcePackageId: request.manifest.id, + sourceVersion: request.manifest.version, + targetPackageId: `${request.manifest.id}-copy`, + namespace: request.manifest.namespace, + copiedCount: 0, + copied: [], + strippedLocks: [], + })); const services: Record = { manifest: { register }, auth: installerAuthService(), objectql: withInstallerGrants({ syncSchemas, registry: { getAllPackages: () => registry } }), + ...(opts.unmanagedImport ? { protocol: { importUnmanagedPackage } } : {}), }; const ctx = { hook: (e: string, h: any) => hooks.set(e, h), @@ -520,3 +531,67 @@ describe('[#26480] a package AHEAD of this runtime is told to upgrade, never to expect(said[0]).toMatch(new RegExp(`upgrade the runtime to protocol ${NEW_MAJOR}`, 'i')); }); }); + +describe('[#30326] an install the handshake admitted UNCHECKED says so in its answer, not only in the server log', () => { + /** The card's reproduction: a package that declares no protocol range at all. */ + const UNDECLARED_ID = 'com.example.qanorange'; + const UNDECLARED = { id: UNDECLARED_ID, name: 'No range', version: '6.0.0', scope: 'project', type: 'app' }; + + /** The notice the answer carries for a package the handshake could not check. */ + function expectSkipped(res: { payload: any; status: number }, expected: Record) { + expect(res.status, JSON.stringify(res.payload)).toBe(200); + expect(res.payload.success).toBe(true); + const notice = res.payload.data.protocolCheckSkipped; + expect(notice, JSON.stringify(res.payload.data)).toBeDefined(); + const { message, ...machine } = notice; + expect(machine).toEqual({ protocolVersion: PROTOCOL_VERSION, ...expected }); + expect(message).toContain(`'${expected.packageId}'`); + expect(message).toContain(`protocol ${PROTOCOL_VERSION} without a compatibility check`); + } + + it('no engines.protocol (inline manifest): 200 with `protocolCheckSkipped` reason `no-range`, and the log line stays', async () => { + const h = await mount(dir); + const res = await h.install({ manifest: UNDECLARED }); + expectSkipped(res, { reason: 'no-range', packageId: UNDECLARED_ID }); + expect(h.installRegistrations().map((m) => m.id)).toEqual([UNDECLARED_ID]); + const said = h.logger.warn.mock.calls.map(([m]) => String(m)).filter((m) => m.includes('[protocol]')); + expect(said).toHaveLength(1); + expect(said[0]).toContain(`package '${UNDECLARED_ID}' declares no engines.protocol range`); + }); + + it('the compiled artifact `os package install ` sends answers the same notice', async () => { + const h = await mount(dir); + const res = await h.install({ + manifest: { manifest: UNDECLARED, id: UNDECLARED_ID, version: UNDECLARED.version, objects: [] }, + }); + expectSkipped(res, { reason: 'no-range', packageId: UNDECLARED_ID }); + }); + + it('a range the handshake cannot read: reason `unparsed-range`, with the range and where it was declared', async () => { + const h = await mount(dir); + const res = await h.install({ manifest: { ...UNDECLARED, engines: { protocol: 'workspace:*' } } }); + expectSkipped(res, { + reason: 'unparsed-range', + packageId: UNDECLARED_ID, + requiredRange: 'workspace:*', + rangeSource: 'engines.protocol', + }); + }); + + it('an `unmanaged` install of the same package carries the notice too', async () => { + const h = await mount(dir, { unmanagedImport: true }); + const res = await h.install({ + manifest: { ...UNDECLARED, namespace: 'qanorange', installModes: ['managed', 'unmanaged'] }, + installMode: 'unmanaged', + }); + expect(res.payload.data?.installMode, JSON.stringify(res.payload)).toBe('unmanaged'); + expectSkipped(res, { reason: 'no-range', packageId: UNDECLARED_ID }); + }); + + it(`control: under ${CURRENT_RANGE} the range is checked, and the answer has no \`protocolCheckSkipped\` key`, async () => { + const h = await mount(dir); + const res = await h.install({ manifest: { ...UNDECLARED, engines: { protocol: CURRENT_RANGE } } }); + expect(res.status, JSON.stringify(res.payload)).toBe(200); + expect(Object.keys(res.payload.data)).not.toContain('protocolCheckSkipped'); + }); +}); diff --git a/packages/metadata-core/src/protocol-handshake.test.ts b/packages/metadata-core/src/protocol-handshake.test.ts index 1c001fc1e37..8c420b2a1e9 100644 --- a/packages/metadata-core/src/protocol-handshake.test.ts +++ b/packages/metadata-core/src/protocol-handshake.test.ts @@ -544,3 +544,24 @@ describe('[#26546] the refusal reads the comparator direction, so an exclusive b }); }); }); + +describe('[#30326] assertProtocolCompat answers the verdict it admitted the package under', () => { + it('no-range: the handshake\'s own verdict, naming the protocol the package loads under unchecked', () => { + expect(assertProtocolCompat({ id: 'p' }, RT, () => {})).toEqual({ + status: 'no-range', runtimeMajor: 11, protocolVersion: RT, + }); + }); + + it('unparsed-range: the verdict with the range and its source', () => { + expect(assertProtocolCompat({ id: 'p', engines: { protocol: '???' } }, RT, () => {})).toEqual({ + status: 'unparsed-range', runtimeMajor: 11, protocolVersion: RT, requiredRange: '???', source: 'engines.protocol', + }); + }); + + it('ok: the checked verdict, equal to checkProtocolCompat\'s for the same manifest', () => { + const manifest = { id: 'p', engines: { protocol: '^11' } }; + const admitted = assertProtocolCompat(manifest, RT, () => {}); + expect(admitted.status).toBe('ok'); + expect(admitted).toEqual(checkProtocolCompat(manifest, RT)); + }); +}); diff --git a/packages/metadata-core/src/protocol-handshake.ts b/packages/metadata-core/src/protocol-handshake.ts index d918d6077d8..72f17f7a768 100644 --- a/packages/metadata-core/src/protocol-handshake.ts +++ b/packages/metadata-core/src/protocol-handshake.ts @@ -49,8 +49,20 @@ export interface ProtocolHandshakeManifest { export type ProtocolCompatResult = | { status: 'ok'; runtimeMajor: number; requiredRange: string; source: RangeSource } - | { status: 'no-range'; runtimeMajor: number } - | { status: 'unparsed-range'; runtimeMajor: number; requiredRange: string; source: RangeSource } + | { + status: 'no-range'; + runtimeMajor: number; + /** The protocol the package is loaded under unchecked -- the version it was judged against. */ + protocolVersion: string; + } + | { + status: 'unparsed-range'; + runtimeMajor: number; + /** The protocol the package is loaded under unchecked -- the version it was judged against. */ + protocolVersion: string; + requiredRange: string; + source: RangeSource; + } | { status: 'incompatible'; runtimeMajor: number; @@ -477,11 +489,11 @@ export function checkProtocolCompat( const runtimeMajor = leadingMajor(protocolVersion) ?? 0; const declared = resolveDeclaredRange(manifest); - if (!declared) return { status: 'no-range', runtimeMajor }; + if (!declared) return { status: 'no-range', runtimeMajor, protocolVersion }; const admits = rangeAdmitsMajor(declared.range, runtimeMajor); if (admits === null) { - return { status: 'unparsed-range', runtimeMajor, requiredRange: declared.range, source: declared.source }; + return { status: 'unparsed-range', runtimeMajor, protocolVersion, requiredRange: declared.range, source: declared.source }; } if (admits) { return { status: 'ok', runtimeMajor, requiredRange: declared.range, source: declared.source }; @@ -530,37 +542,45 @@ export function checkProtocolCompat( /** Warn hook — overridable for tests; defaults to `console.warn`. */ export type WarnFn = (message: string) => void; +/** The verdicts {@link assertProtocolCompat} admits: every one but `incompatible`, which it throws. */ +export type ProtocolCompatAdmitted = Exclude; + /** - * Enforce the handshake at a load/install boundary. + * Enforce the handshake at a load/install boundary, and answer the verdict it + * admitted the package under. * * - `ok` → returns silently. * - `no-range` → warns once (grandfathering; `objectstack lint` nudges the * package to declare a range, and scaffolds stamp one going forward). * - `unparsed-range` → warns (parser-coverage gap; never a false rejection). * - `incompatible` → throws {@link ProtocolIncompatibleError}. + * + * The returned verdict is {@link checkProtocolCompat}'s own, handed back so a + * door that answers a caller can say the check was skipped (`no-range`, + * `unparsed-range`) without judging the range a second time. */ export function assertProtocolCompat( manifest: ProtocolHandshakeManifest, protocolVersion: string = PROTOCOL_VERSION, warn: WarnFn = (m) => console.warn(m), -): void { +): ProtocolCompatAdmitted { const result = checkProtocolCompat(manifest, protocolVersion); const pkg = manifest.id ?? ''; switch (result.status) { case 'ok': - return; + return result; case 'no-range': warn( `[protocol] package '${pkg}' declares no engines.protocol range; ` + `loading under protocol ${protocolVersion} without a compatibility check (ADR-0087).`, ); - return; + return result; case 'unparsed-range': warn( `[protocol] package '${pkg}' declares an unrecognized ${result.source} range ` + `'${result.requiredRange}'; skipping the protocol handshake (ADR-0087).`, ); - return; + return result; case 'incompatible': throw new ProtocolIncompatibleError(result.diagnostic); }