Skip to content

CLI KB version check: installed vs current + migration pointer #261

Description

@rucka

Story Statement

As a CLI user maintaining a project with an installed pair KB
I want the CLI to compare the installed KB version against the current/available one and surface drift
So that version mismatch is visible and points me to the right migration page — before it breaks skills

Where: pair-cli (kb-info extension + install/update hint)

Epic Context

Parent Epic: Versioning & migrazione adoption/KB #214
Status: Refined
Priority: P0 (Must-Have)

Status Workflow

  • Refined: Story is detailed, estimated, and ready for development
  • In Progress: Story is actively being developed
  • Done: Story delivered and accepted

Acceptance Criteria

Functional Requirements

Given-When-Then Format:

  1. Given an installed KB older than the source's current version
    When the user runs kb-info (or an explicit version check)
    Then the CLI reports installed vs current versions, flags the mismatch, and prints the migration-docs URL for the jump

  2. Given an installed KB matching the current version
    When the check runs
    Then the CLI reports up-to-date, exit code 0

  3. Given install/update about to apply a version with breaking changes
    When the operation starts
    Then the CLI surfaces the mismatch hint (non-blocking) with the migration page pointer

  4. Given a KB installed without version metadata (legacy install)
    When the check runs
    Then the CLI degrades gracefully: reports "unknown installed version" and suggests re-install/update to record it

Business Rules

  • Check compares metadata only — the CLI performs NO migration (D20)
  • Works for official and external KB sources (version from package/source manifest)
  • Machine-readable output (JSON flag) consistent with existing kb-info

Edge Cases and Error Handling

  • Source unreachable: report installed version, mark current as unavailable, exit 0 (no hard failure offline)
  • Pre-release/dev versions: compared verbatim, labeled as non-stable
  • Multiple registries: version reported per registry where applicable

Definition of Done Checklist

Development Completion

  • All acceptance criteria implemented and verified
  • Automated tests written and passing (fixtures: match, drift, legacy, offline)
  • Code review completed and approved
  • Documentation updated — docs site (apps/website): CLI reference for the check
  • CLI (apps/pair-cli) updated — this story is CLI work by definition

Quality Assurance

  • Round-trip: install old fixture → check flags drift → update → check clean
  • JSON output schema validated

Story Sizing and Sprint Readiness

Refined Story Points

Final Story Points: 3 (M)
Confidence Level: High
Sizing Justification: kb-info already reads installed metadata; delta = current-version resolution + comparison + messaging — confirmed M(3)

Sprint Capacity Validation

Sprint Fit Assessment: yes, single sprint
Total Effort Assessment: fits — Yes

Dependencies and Coordination

Story Dependencies

Prerequisite Stories: none (metadata base exists in kb-info)
Dependent Stories: migration pages story (URL target); dogfood migration (uses the check to verify)

Validation and Testing Strategy

Acceptance Testing Approach

Testing Methods: unit + CLI integration tests with version fixtures; offline-mode test; manual smoke on the pair repo
Test Data Requirements: KB fixtures at two versions; one legacy (no metadata)

Notes and Additional Context

Refinement Session Insights: D20 — check-only scope; base already in kb-info; mismatch message must name the exact migration page (one per jump)
Documentation Links: D20 · Spec G11 · epic AC2/AC3

Technical Analysis

Implementation Approach

Technical Strategy: extend kb-info with current-version resolution from the configured source (registry/URL/--source), add comparison + human and JSON outputs; hook a non-blocking hint into install/update flows
Key Components: version resolver (per source type), comparator, message formatter, migration-URL mapper (version jump → docs path)
Integration Points: existing kb-info, install/update commands, docs Migrations index

Technical Risks and Mitigation

Risk Impact Probability Mitigation Strategy
Source types report versions inconsistently Medium Medium normalize in the resolver; verbatim fallback with "non-comparable" label
URL mapping drifts from docs structure Low Medium single mapping helper + docs index test fixture

Task Breakdown

Checklist

  • T1 — Current-version resolver per source type (registry/URL/local)
  • T2 — Comparison + human/JSON output in kb-info
  • T3 — Non-blocking mismatch hint in install/update + migration-URL mapper
  • T4 — Fixtures + integration tests (match/drift/legacy/offline) + docs

Dependency Graph

T1 → T2 → T3 → T4

AC Coverage

Task AC1 AC2 AC3 AC4
T1
T2
T3
T4

T-1: Current-version resolver per source type (registry/URL/local)

Priority: P0 | Estimated Hours: 4h | Bounded Context: Integration & Process Standardization

Summary: Resolve the "current" (available) KB version for any of the source kinds pair-cli already accepts (registry default, remote URL, local path/zip, git), plus read/write helpers for the version recorded as installed in a project.

Type: Feature Implementation

Description: Implemented in apps/pair-cli/src/commands/kb-info/version-resolver.ts. resolveCurrentVersion(fs, options) detects the source kind from an optional --source value (detectSourceKind: git URL → git, remote URL → remote, path → local, absent → registry) and dispatches to a per-kind resolver, never throwing — unexpected errors degrade to { available: false, error }. readVersionFromDirectory reads a version from manifest.json at a directory root, falling back to a sibling package.json (monorepo packages/knowledge-hub/dataset layout). resolveRegistryVersion mirrors install/update's own dataset resolution via resolveDatasetRoot(fs, { resolution: 'default' }, { cliVersion, httpClient }) — this is a fix (commit b442f54) over the original implementation, which called the monorepo dataset path directly and threw for real npm-installed (non-monorepo) users; it now falls back to download/cache exactly like install/update do, with cliVersion threaded through dispatcher → handler → resolver. resolveLocalVersion reads a directory or a .zip (via readManifestFromZip, shared with kb-info's package-display mode). resolveRemoteVersion extracts a version from the URL itself and probes reachability via an injected HttpClientService (offline/no-client degrades to unavailable, per AC "Source unreachable"). resolveInstalledVersion/writeInstalledVersion read/write a .pair/.kb-version.json marker ({ version, recordedAt }); a missing or malformed marker degrades to { version: null } (AC4, legacy installs). isStableVersion flags plain major.minor.patch as stable, anything else (pre-release/build tags) as non-stable.

Acceptance Criteria:

  • Primary deliverable: resolveCurrentVersion returns { sourceKind, version, available, stable, error? } for registry/local/remote/git sources; resolveInstalledVersion/writeInstalledVersion round-trip the installed-version marker.
  • Quality standard: never throws — all source resolvers degrade to { available: false, error } on failure (offline, missing path, bad zip, unsupported source).
  • Integration requirement: registry resolution reuses resolveDatasetRoot's 'default' resolution (same code path as install/update), not a bespoke monorepo-only lookup.
  • Verification method: unit tests per source kind in version-resolver.test.ts, including the release-context (non-monorepo, download-fallback) scenario.

Implementation Approach:

  • Technical Design: single resolveCurrentVersion entrypoint dispatching on KbSourceKind; each source resolver returns a partial result merged with sourceKind/stable by the entrypoint so isStableVersion is applied uniformly.
  • Files to Modify/Create:
    • apps/pair-cli/src/commands/kb-info/version-resolver.ts — resolver, marker read/write, stability check (new)
    • apps/pair-cli/src/commands/kb-info/manifest-reader.tsreadManifestFromZip, extracted/shared with kb-info package mode (new)
    • apps/pair-cli/src/commands/kb-info/handler.ts — thread cliVersion/httpClient into the resolver (touched by the b442f54 fix)
    • .gitignore — ignore .pair/.kb-version.json (untracked dogfood artifact)

Dependencies:

  • Tasks: none (first task in the chain)

Implementation Steps:

  1. Define KbSourceKind, CurrentVersionResult, InstalledVersionResult types and detectSourceKind.
  2. Implement readVersionFromDirectory (manifest.json → sibling package.json fallback) and isStableVersion.
  3. Implement per-source resolvers (resolveRegistryVersion, resolveLocalVersion, resolveRemoteVersion, git stub) and the resolveCurrentVersion dispatcher with try/catch degradation.
  4. Implement resolveInstalledVersion/writeInstalledVersion against the .pair/.kb-version.json marker.
  5. Fix resolveRegistryVersion to use resolveDatasetRoot's 'default' resolution instead of a direct monorepo-only path, threading cliVersion through from the dispatcher; gitignore the marker file.

Testing Strategy:

  • Unit Tests: version-resolver.test.tsisStableVersion, readVersionFromDirectory (manifest vs sibling package.json vs neither), registry source (available / unavailable / release-context download-fallback with isKBCached/ensureKBAvailable mocked), local source (dir/zip/missing), remote source (reachable/unreachable/no-client), git source (graceful degrade), installed-version marker round-trip (missing/present/malformed).
  • Manual Testing: none beyond the automated suite for this task — manual smoke is covered by T4.

Notes: The registry-fallback fix and the .gitignore entry (both from commit b442f54) are genuinely present on this branch — verified in the current diff and by the added version-resolver.test.ts case "falls back to download in a real release install (no monorepo, no local dataset, network available)". No open gap for this task specifically.


T-2: Comparison + human/JSON output in kb-info

Priority: P0 | Estimated Hours: 3h | Bounded Context: Integration & Process Standardization

Summary: Add a version-check mode to pair kb-info (triggered when no package path is given) that compares installed vs. current version and prints a human-readable or JSON result.

Type: Feature Implementation

Description: apps/pair-cli/src/commands/kb-info/parser.ts now returns a discriminated KbInfoCommandConfig (mode: 'package' | 'version-check'): a positional arg selects package mode (unchanged behavior), its absence selects version-check mode with optional --json/--source. version-check.ts's compareVersions(installed, current) is a pure function producing one of four statuses: up-to-date (equal, known versions), drift (both known, differ — attaches migrationUrl via buildMigrationUrl), unknown-installed (no installed marker — checked first, AC4), current-unavailable (installed known but current not resolvable — AC offline case). version-check-formatter.ts renders this as either a human block (formatVersionCheckHuman, includes status/installed/current/source lines, a "(non-stable)" suffix, migration-guide line for drift, remediation copy for unknown/unavailable) or raw JSON (formatVersionCheckJSON, just JSON.stringify(result) — the whole VersionCheckResult shape, consistent with existing kb-info --json behavior in package mode). handler.ts's handleVersionCheckMode wires resolveInstalledVersion + resolveCurrentVersion + compareVersions + the formatter, and always returns exit code 0 (this is a check, never a hard failure — matches AC2's "exit code 0" and the docs' explicit "Exit code is 0 in all cases"). dispatcher.ts was updated to pass opts (containing httpClient/baseTarget/cliVersion) into the kb-info handler call, which it previously didn't need for package-only mode.

Acceptance Criteria:

  • Primary deliverable: pair kb-info (no path) prints installed/current/status/source, JSON via --json, with an explicit migration URL on drift.
  • Quality standard: exit code 0 for all four statuses (up-to-date/drift/unknown-installed/current-unavailable) — this is metadata-only, no hard failures (D20).
  • Integration requirement: JSON output is the full VersionCheckResult object (status, installed, current, optional migrationUrl), machine-consumable and consistent with the existing package-mode --json.
  • Verification method: version-check.test.ts (pure comparator, all 4+ statuses incl. pre-release verbatim compare) and handler.test.ts's version-check mode describe block (end-to-end through handleKbInfoCommand, incl. remote --source), plus version-check-formatter.test.ts and parser.test.ts.

Implementation Approach:

  • Technical Design: strict separation between comparison (pure, version-check.ts) and presentation (version-check-formatter.ts), matching the codebase's existing display-formatter.ts split for package mode.
  • Files to Modify/Create:
    • apps/pair-cli/src/commands/kb-info/version-check.tscompareVersions, VersionCheckStatus/VersionCheckResult (new)
    • apps/pair-cli/src/commands/kb-info/version-check-formatter.ts — human/JSON renderers (new)
    • apps/pair-cli/src/commands/kb-info/handler.tshandleVersionCheckMode, kept handlePackageMode unchanged
    • apps/pair-cli/src/commands/kb-info/parser.ts — discriminated config, --source support
    • apps/pair-cli/src/commands/kb-info/metadata.ts — usage/examples/notes updated for the new mode
    • apps/pair-cli/src/commands/dispatcher.ts — pass opts through to kb-info

Dependencies:

  • Tasks: T1 (resolver + marker read must exist first)

Implementation Steps:

  1. Extend parser.ts with the mode discriminant and --source passthrough; drop the old "package path is required" throw.
  2. Implement compareVersions with the four-status decision order (unknown-installed checked before current-unavailable, per AC4 precedence).
  3. Implement formatVersionCheckHuman/formatVersionCheckJSON.
  4. Wire handleVersionCheckMode into handleKbInfoCommand, threading baseTarget/httpClient/cliVersion options.
  5. Update dispatcher.ts to pass opts to the kb-info handler and metadata.ts's CLI help text.

Testing Strategy:

  • Unit Tests: version-check.test.ts (match/drift/legacy/offline fixtures + pre-release verbatim), version-check-formatter.test.ts (all four statuses' human output + JSON round-trip), parser.test.ts (mode discrimination, --source/--json passthrough).
  • Manual Testing: handler.test.ts's version-check describe block exercises the full flow through handleKbInfoCommand with an in-memory fs and a mock HTTP client (registry match, registry drift with JSON, legacy, registry-unavailable, remote unreachable, remote reachable) — no separate manual smoke was run for this task beyond that.

Notes: The AC "Multiple registries: version reported per registry where applicable" business rule is not implemented anywhere in this task (or elsewhere on the branch) — resolveCurrentVersion/compareVersions operate on a single resolved source/version, with no notion of iterating the project's configured asset_registries and reporting a version per registry. This is a genuine, unaddressed gap against the story's Business Rules section, not just a documentation nit. Also, "JSON output schema validated" (DoD) is only informally covered — tests assert toMatchObject shape expectations, not validation against a formal JSON schema document.


T-3: Non-blocking mismatch hint in install/update + migration-URL mapper

Priority: P0 | Estimated Hours: 2.5h | Bounded Context: Integration & Process Standardization

Summary: Print a non-blocking drift hint at the start of install/update when the version about to be applied differs from the recorded installed version, and record the newly-applied version on success.

Type: Feature Implementation

Description: migration-url.ts is the single source of truth for the docs URL pattern (D20: one page per version jump, no migration logic in the CLI): buildMigrationUrl(from, to)${DOCS_MIGRATIONS_BASE}/v{from}-to-v{to} (strips a leading v from either input), migrationsIndexUrl() returns the base index page. version-hint.ts exposes two best-effort, never-throwing helpers: emitVersionDriftHint({ fs, datasetRoot, baseTarget, presenter }) reads the new version from the about-to-be-applied dataset (readVersionFromDirectory), reads the recorded installed version (resolveInstalledVersion), and — only if both are known and differ — prints a presenter.phase(...) line with the migration URL; it silently no-ops if either version is unknown or they match. recordInstalledVersion({ fs, datasetRoot, baseTarget }) writes the just-applied version via writeInstalledVersion after a successful install/update, so future kb-info checks have something to compare against (closing the AC4 "unknown installed version" loop). Both handlers (install/handler.ts, update/handler.ts) call emitVersionDriftHint right after resolving the dataset root/registries/baseTarget and before validating/executing the copy, and call recordInstalledVersion at the very end of their success path (executeInstall/runUpdateSequence), after writeProjectLlmsTxt.

Acceptance Criteria:

  • Primary deliverable: install/update print a "KB version drift" heads-up (with migration URL) when a prior recorded version differs from the version being applied, and always record the applied version on success.
  • Quality standard: the hint is genuinely non-blocking — any resolution failure inside emitVersionDriftHint/recordInstalledVersion is swallowed (try/catch, no rethrow) and never aborts install/update.
  • Integration requirement: uses the same migration-url.ts mapper as kb-info's drift status — one URL-building implementation, not duplicated logic.
  • Verification method: install/handler.test.ts and update/handler.test.ts's KB version recording (#261) describe blocks (marker written, hint printed, no marker written when the source has no version metadata).

Implementation Approach:

  • Technical Design: hint emission is a pure side-effecting read (never mutates), recording is a pure side-effecting write; both delegate to version-resolver.ts primitives from T1 rather than re-implementing marker I/O.
  • Files to Modify/Create:
    • apps/pair-cli/src/commands/kb-info/migration-url.tsbuildMigrationUrl, migrationsIndexUrl (new)
    • apps/pair-cli/src/commands/kb-info/version-hint.tsemitVersionDriftHint, recordInstalledVersion (new)
    • apps/pair-cli/src/commands/install/handler.ts — hook both calls into handleInstallCommand/executeInstall
    • apps/pair-cli/src/commands/update/handler.ts — hook both calls into handleUpdateCommand/runUpdateSequence

Dependencies:

  • Tasks: T1 (resolver/marker primitives), T2 (shares migration-url.ts with the kb-info drift status — built alongside/after so the mapper is the single shared implementation)

Implementation Steps:

  1. Implement buildMigrationUrl/migrationsIndexUrl in migration-url.ts.
  2. Implement emitVersionDriftHint (read new + installed version, compare, print via CliPresenter.phase, swallow all errors).
  3. Implement recordInstalledVersion (read new version, write marker, swallow all errors).
  4. Hook emitVersionDriftHint into install/handler.ts (after setupInstallContext, before validation) and update/handler.ts (after setupUpdateContext, before validation).
  5. Hook recordInstalledVersion into both handlers' success paths, after writeProjectLlmsTxt.

Testing Strategy:

  • Unit Tests: migration-url.test.ts (URL shape, leading-v stripping, index nesting); install/handler.test.ts and update/handler.test.ts "CLI KB version check: installed vs current + migration pointer #261" describe blocks (marker recorded on success, no marker when source lacks version metadata, drift hint text + migration URL printed on console when a prior differing version is recorded, install/update still completes successfully alongside the hint).
  • Manual Testing: none beyond the automated coverage above — no manual smoke test was run for the install/update hint on a real project in this task.

Notes: No discrepancy found for this task's own scope — the hint and recording behavior match what's tested. However, the story-level DoD item "Round-trip: install old fixture → check flags drift → update → check clean" is not implemented as a single test anywhere on the branch: install/handler.test.ts and update/handler.test.ts each test the hint/recording in isolation, and handler.test.ts's kb-info tests exercise the comparator independently, but no test chains install(old version) → kb-info (asserts drift) → update(new version) → kb-info (asserts up-to-date/clean) end-to-end. This is a real, open gap against the story's Quality Assurance checklist, not just a nice-to-have.


T-4: Fixtures + integration tests (match/drift/legacy/offline) + docs

Priority: P1 | Estimated Hours: 2h | Bounded Context: Integration & Process Standardization

Summary: Cover all four version-check scenarios (match/drift/legacy/offline) end-to-end and document the new kb-info mode in the CLI reference.

Type: Testing / Documentation

Description: Rather than separate fixture files, the four required scenarios are expressed as inline InMemoryFileSystemService setups directly in each test file (a pattern already used throughout pair-cli's test suite): version-resolver.test.ts and version-check.test.ts each construct match/drift/legacy/offline states at the unit level, and handler.test.ts's "version-check mode" describe block re-exercises the same four scenarios end-to-end through handleKbInfoCommand (in-memory fs + MockHttpClientService), including both human and --json output for each. Docs were updated in apps/website/content/docs/reference/cli/commands.mdx: the kb-info section now documents the version-check mode (usage, --source option, the four status outcomes, exit-code-always-0 behavior, and the install/update hint), plus new examples (pair kb-info, pair kb-info --json, pair kb-info --source /path/to/kb) and an updated one-line command-list description.

Acceptance Criteria:

  • Primary deliverable: all four scenarios (match/drift/legacy/offline) are exercised both at the pure-comparator level and end-to-end through the CLI handler, in both human and JSON output modes.
  • Quality standard: tests use realistic in-memory fixtures (real manifest.json/package.json shapes, real marker-file JSON) rather than mocked-away comparator inputs only.
  • Integration requirement: docs site (apps/website) CLI reference reflects the new command surface (--source, version-check mode, exit-code semantics).
  • Verification method: pnpm --filter pair-cli test (all new/updated *.test.ts files pass); manual read-through of the rendered commands.mdx section.

Implementation Approach:

  • Technical Design: reuse existing in-memory fs/mock-http test utilities (InMemoryFileSystemService, MockHttpClientService, buildTestResponse/toIncomingMessage) already present in the @pair/content-ops test-support package; no new fixture-file format introduced.
  • Files to Modify/Create:
    • apps/pair-cli/src/commands/kb-info/version-resolver.test.ts — per-source-kind scenarios (extended)
    • apps/pair-cli/src/commands/kb-info/version-check.test.ts — comparator match/drift/legacy/offline + pre-release (new)
    • apps/pair-cli/src/commands/kb-info/handler.test.ts — end-to-end version-check describe block (extended)
    • apps/pair-cli/src/commands/dispatcher.test.ts — version-check mode dispatches without throwing (extended)
    • apps/website/content/docs/reference/cli/commands.mdx — kb-info docs (extended)

Dependencies:

  • Tasks: T1, T2, T3 (tests exercise all three layers; docs describe the combined behavior)

Implementation Steps:

  1. Add match/drift/legacy/offline cases to version-resolver.test.ts and version-check.test.ts.
  2. Extend handler.test.ts with the "version-check mode" describe block covering all four statuses in both output formats, plus remote --source reachable/unreachable cases.
  3. Extend dispatcher.test.ts to confirm kb-info version-check mode dispatches (no path arg) without throwing.
  4. Update commands.mdx with the new usage, options table, "Version Check Mode" subsection, and examples.

Testing Strategy:

  • Unit Tests: as listed above — this task is itself the test-writing task; see T1–T3 for what each layer actually asserts.
  • Manual Testing: no manual smoke test against a real installed pair project was performed/recorded for this story (the story's "manual smoke on the pair repo" testing-strategy line is not evidenced by any script, log, or note on the branch).

Notes: Two real gaps to flag honestly, both already called out in T2/T3 but restated here since T4 owns "tests + docs" in the checklist: (1) no single round-trip test chains install(old)→check(drift)→update→check(clean) as required by the story's Quality Assurance checklist — the coverage is real but fragmented across separate unit/handler tests, not one end-to-end test; (2) the "multiple registries: version reported per registry" business rule has no test (and no implementation) anywhere — there was nothing to write a fixture for. Additionally, the story's "manual smoke on the pair repo" testing method has no corresponding evidence on the branch (no CHANGELOG note, no manual test log) — this may have been done ad hoc but is not verifiable from the diff.

Metadata

Metadata

Assignees

No one assigned

    Labels

    user storyWork item representing a user story

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions