feat(#92): unpinned-spec detection + resolved version + one-click pin + drift (P0) #98

Closed
the_og wants to merge 1 commits from feat/92 into feat/90
Owner

What (P0 — correctness)

npx -y ssh-mcp resolves latest on every launch. In a single working session the package went v1 → v2 and the exposed tool set changed under a running agent, mid-task, with no warning — "my AI tools broke today for no reason". This detects the unpinned spec and turns it into a comprehensible pin/upgrade prompt.

Core (pure bcc_core, no network — reuses parse_version / is_newer_version / the catalog spec parsers)

  • server_package_spec / server_package_name — the npm spec an npx-style server launches.
  • is_unpinned_spec — a bare name or dist-tag (@latest/@next) is unpinned; an exact numeric version is pinned.
  • resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins), degrades to None cleanly. find/read are injectable so tests drive it with fixtures — never shells out, never hits the network, never touches a real node_modules in CI.
  • pin_spec_transform — rewrites the spec to name@version (mirrors pin_command_path's (new_data, note) contract). No-op when already pinned / bad version / not npx.
  • version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version (so 2 < 10, never lexical).
  • version_status — the badge's high-level dict (unpinned / pinned_version / resolved_version / can_pin / drift). A _UNSET sentinel lets a caller force an explicit resolved=None ("unknown") vs. omitting it to run the local lookup.

GUI (minimal, mirrors the dependency-status surface)

A version badge row under the dependency status, shown only for npx servers: the currently-resolved version, an "unpinned (resolves latest each launch)" warning, a drift note when a pin has been overtaken, and a one-click "Pin to <version>" button (only when unpinned and a resolved version is known). Smoke-tested headlessly with QT_QPA_PLATFORM=offscreen.

Testing

ruff + format --check clean; pytest green (520 passed). Fixture-driven tests for spec detection, unpinned classification, package.json parsing, cache resolution (highest-wins + degrade-to-unknown), the pin transform (incl. no-op cases), numeric drift, and every version_status branch.

Closes #92. Part of epic #94.


Stacked PR — base is feat/90 (depends only on the ServerSpec spine — sibling of #91, not built on it). Review/merge #90 first, then this (in either order relative to #91).
🤖 Generated with Claude Code

## What (P0 — correctness) `npx -y ssh-mcp` resolves **latest on every launch**. In a single working session the package went v1 → v2 and the exposed tool set changed **under a running agent, mid-task**, with no warning — "my AI tools broke today for no reason". This detects the unpinned spec and turns it into a comprehensible pin/upgrade prompt. ### Core (pure `bcc_core`, **no network** — reuses `parse_version` / `is_newer_version` / the catalog spec parsers) - **`server_package_spec` / `server_package_name`** — the npm spec an npx-style server launches. - **`is_unpinned_spec`** — a bare name or dist-tag (`@latest`/`@next`) is unpinned; an exact numeric version is pinned. - **`resolved_npx_version`** — reads the local `~/.npm/_npx` cache (highest version wins), degrades to `None` cleanly. `find`/`read` are injectable so tests drive it with fixtures — **never shells out, never hits the network, never touches a real `node_modules` in CI**. - **`pin_spec_transform`** — rewrites the spec to `name@version` (mirrors `pin_command_path`'s `(new_data, note)` contract). No-op when already pinned / bad version / not npx. - **`version_drift_note`** — *"moved X → Y since you pinned"* via numeric `is_newer_version` (so 2 < 10, never lexical). - **`version_status`** — the badge's high-level dict (`unpinned` / `pinned_version` / `resolved_version` / `can_pin` / `drift`). A `_UNSET` sentinel lets a caller force an explicit `resolved=None` ("unknown") vs. omitting it to run the local lookup. ### GUI (minimal, mirrors the dependency-status surface) A version badge row under the dependency status, shown **only for npx servers**: the currently-resolved version, an "unpinned (resolves latest each launch)" warning, a drift note when a pin has been overtaken, and a one-click **"Pin to `<version>`"** button (only when unpinned *and* a resolved version is known). Smoke-tested headlessly with `QT_QPA_PLATFORM=offscreen`. ### Testing `ruff` + `format --check` clean; `pytest` green (520 passed). Fixture-driven tests for spec detection, unpinned classification, package.json parsing, cache resolution (highest-wins + degrade-to-unknown), the pin transform (incl. no-op cases), numeric drift, and every `version_status` branch. Closes #92. Part of epic #94. --- **Stacked PR — base is `feat/90`** (depends only on the ServerSpec spine — sibling of #91, **not** built on it). Review/merge **#90 first**, then this (in either order relative to #91). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
the_og added 1 commit 2026-08-12 03:05:13 -04:00
feat(#92): detect unpinned npx specs, resolve version, one-click pin, drift
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 9s
2cd8e0fb3b
`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went
v1 → v2 and the tool set changed under a running agent, mid-task, with no warning.
This detects that and turns it into a comprehensible pin/upgrade prompt.

Core (pure, no network — reuses parse_version / is_newer_version / the catalog
spec parsers; the resolved-version lookup is fully injectable for tests):
- server_package_spec / server_package_name — the npm spec an npx-style server runs.
- is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact
  numeric version is pinned.
- resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins),
  degrades to None cleanly. NO network. find/read injectable.
- pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's
  (new_data, note) contract). No-op when already pinned / bad version / not npx.
- version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version.
- version_status — the badge's high-level dict (unpinned / pinned_version /
  resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an
  explicit resolved=None ("unknown") vs. omitting it to do the local lookup.

GUI: a version badge row under the dependency status (mirrors that surface) with a
one-click "Pin to <version>" button, shown only for npx servers; a drift note when
a pinned version has been overtaken. Smoke-tested headlessly.

pytest green (520 passed), ruff + format clean. Closes #92. Part of epic #94.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
the_og closed this pull request 2026-08-12 23:53:47 -04:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.