Skip to content

refactor: range-based spec version tagging (introducedIn/removedIn)#265

Open
pcarleton wants to merge 2 commits into
mainfrom
paulc/introducedin-removedin
Open

refactor: range-based spec version tagging (introducedIn/removedIn)#265
pcarleton wants to merge 2 commits into
mainfrom
paulc/introducedin-removedin

Conversation

@pcarleton
Copy link
Copy Markdown
Member

@pcarleton pcarleton commented May 8, 2026

Closes #256. Follows up on #255.

Problem

Scenarios were tagged with an explicit list of every spec version they apply to:

specVersions = ['2025-06-18', '2025-11-25']

This means every new spec release requires touching every carried-forward scenario. It also can't express removal — a SEP that tightens or removes an existing requirement has no way to mark an old scenario as "no longer valid in draft".

Changes

Range-based version tagging

src/types.ts: replaced specVersions: ScenarioSpecTag[] on all three scenario interfaces with a range expressed via ScenarioSource:

export type ExtensionId = 'client-credentials' | 'enterprise-managed-auth';

export interface ScenarioSource {
  source?: 'extension';
  extensionId?: ExtensionId;
  introducedIn?: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION;
  removedIn?: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION;
}

The source discriminant replaces the old 'extension' tag. When source is unset (the default), the scenario sits on the dated spec timeline and introducedIn is required; when source === 'extension', the scenario is off-timeline and carries an extensionId instead — no spec-version fields. This avoids the placeholder introducedIn = LATEST_SPEC_VERSION that an earlier draft of this PR put on extension scenarios. The string discriminant leaves room for future off-timeline kinds.

A class cannot implements a union type (TS2422), so ScenarioSource is a flat interface with optional fields; the cross-field invariant is asserted by assertSourceInvariant() in spec-version.test.ts.

src/scenarios/index.ts: rewrote matchesSpecVersion() as a range comparison gated on source:

if (scenario.source !== undefined || scenario.introducedIn === undefined) return false;
introducedIn <= target && (!removedIn || target < removedIn)

'draft' sorts after all dated versions via index lookup into ALL_SPEC_VERSIONS. getScenarioSpecVersions() reconstructs a ScenarioSpecTag[] from the range (returns ['extension'] for source === 'extension') for backward-compat with tier-check output. ALL_SPEC_VERSIONS, resolveSpecVersion, and all --spec-version CLI semantics are unchanged.

Scenario migration (~29 files)

Old specVersions New fields
['2025-03-26', '2025-06-18', '2025-11-25'] introducedIn: '2025-03-26'
['2025-06-18', '2025-11-25'] introducedIn: '2025-06-18'
['2025-11-25'] introducedIn: '2025-11-25'
['2025-03-26'] introducedIn: '2025-03-26', removedIn: '2025-06-18'
[DRAFT_PROTOCOL_VERSION] introducedIn: DRAFT_PROTOCOL_VERSION
['extension'] source: 'extension', extensionId: <id>

cross-app-accessenterprise-managed-auth rename

The extension's canonical name is enterprise-managed-auth. Renamed for consistency: file, class EnterpriseManagedAuthScenario, slug auth/enterprise-managed-auth, context.ts schema literal, and the everything-client.ts runner.

Tests

src/scenarios/spec-version.test.ts rewritten to assert range semantics and the ScenarioSource invariant directly. Superset and isolation tests preserved.

Testing

$ npx vitest run
Test Files  9 passed (9)
     Tests  118 passed (118)

Build, type-check, and lint clean. node dist/index.js list shows auth/enterprise-managed-auth [extension].

… tagging

Replaces per-scenario `specVersions: SpecVersion[]` arrays with `introducedIn`
and optional `removedIn` range fields on all three scenario interfaces. Extension
scenarios get an orthogonal `extension?: boolean` flag instead of the former
`'extension'` tag in the versions list.

matchesSpecVersion() is rewritten as a range comparison
(introducedIn <= target && (!removedIn || target < removedIn)), with draft
sorting after all dated versions. getScenarioSpecVersions() reconstructs the
effective version list from the range for backward-compat with tier-check.
Closes #256.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 8, 2026

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@265

commit: ca22f78

…ionId

Extension scenarios were forced to set a placeholder introducedIn that was
never read (matchesSpecVersion/getScenarioSpecVersions short-circuit on the
flag). Replace `extension?: boolean` with a string `source` discriminant
on ScenarioSource so off-timeline scenarios carry no spec-version fields at
all. Adds typed ExtensionId so each extension scenario names which extension
it exercises.

A class cannot `implements` a union type (TS2422), so ScenarioSource is a
flat interface with optional fields; the cross-field invariant (extension
scenarios must have extensionId and no introducedIn/removedIn; spec-timeline
scenarios must have introducedIn) is asserted in spec-version.test.ts.

Also renames the cross-app-access scenario to enterprise-managed-auth to
match the extension's canonical name: file, class, slug, context schema
literal, and the everything-client runner.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace specVersions list with introducedIn/removedIn range tagging

2 participants