Skip to content

fix(pkg-deps): resolve runtime.version on MCP + reject silent no-op flag combos - #17

Merged
jlitola merged 1 commit into
mainfrom
quiet-harbor
Apr 20, 2026
Merged

fix(pkg-deps): resolve runtime.version on MCP + reject silent no-op flag combos#17
jlitola merged 1 commit into
mainfrom
quiet-harbor

Conversation

@jlitola

@jlitola jlitola commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #16. Three user-facing issues from the final codex review round that didn't make it into the merged commit.

Fixes

  • MCP runtime.items[].version now always resolved. The tool description promises {name, version, constraint} records with the backend resolving each constraint to a concrete version, but the merged code only resolved versions when the caller also set include_transitive: true. Now the handler always fetches the transitive DAG at depth 1 (mirroring the CLI path) so runtime.items[].version resolves regardless of whether the caller asked for the transitive block. The transitive block itself is still emitted only when include_transitive: true. One extra backend round-trip, in exchange for a contract that matches what the description says.

  • Hard-error on silent no-op flag combinations. Three inputs used to be silently ignored when paired incorrectly; each now returns INVALID_ARGUMENT:

    • MCP max_depth without include_transitive
    • MCP include_importers without include_transitive
    • CLI --depth without --transitive

    On an agent-facing tool, silently-ignored inputs are worse than a rejection: the caller believes they got the effect they asked for.

  • Stale documentation from the UX-redesign rounds.

    • tools.md tool-table row still claimed package_dependencies returned "an opaque DAG" — we dropped transitive.dag from the envelope mid-iteration.
    • tools.md MCP-schema note still said "CLI's --depth defaults to 3"; --depth has no default, backend full-graph traversal wins.
    • cli-commands.md two-views section mentioned NuGet TFMs in the pkg deps context, but NuGet is rejected client-side for this tool.

Cleanup

Envelope builder (src/shared/package-dependencies-response.ts) had ~155 lines of dead code left behind after the envelope took over provenance decoding from the terminal formatter: DecodedConflict, decodeConflictEntry, decodeCycleEntry, ProvenanceEntry, buildProvenanceLookup. Deleted. Module header rewritten to describe the current state (preprocessed transitive, typed conflicts/cycles with raw fallback, no transitive.dag, no uniqueDependencies).

Tests

  • bun test — 962 pass, 0 fail (was 958; added 4 new tests).
  • bun run typecheck / bun run build clean.
  • Three new tests:
    • MCP: runtime.items[].version resolves even when include_transitive is absent (locks the wire behaviour).
    • MCP: max_depth without include_transitive returns INVALID_ARGUMENT.
    • MCP: include_importers without include_transitive returns INVALID_ARGUMENT.
    • CLI: --depth without --transitive errors with the expected hint.

Test plan

  • `bun test` passes (962 / 0)
  • `bun run typecheck` clean
  • `bun run build` clean
  • MCP smoke: `package_dependencies` without `include_transitive` returns `runtime.items[].version` populated
  • MCP smoke: `max_depth: 3` alone returns `INVALID_ARGUMENT`
  • CLI smoke: `pkg deps npm:express --depth 3` (no `--transitive`) errors with the hint

🤖 Generated with Claude Code

…lag combos

Follow-up to #16. Three user-facing issues from the final codex review
round that didn't make it into the merged commit.

## Fixes

- **MCP `runtime.items[].version` now always resolved.** The tool
  description promises `{name, version, constraint}` records with the
  backend resolving each constraint to a concrete version, but the
  merged code only resolved versions when the caller also set
  `include_transitive: true`. Now the handler always fetches the
  transitive DAG at depth 1 (mirroring the CLI path) so
  `runtime.items[].version` resolves regardless of whether the caller
  asked for the `transitive` block. The `transitive` block itself is
  still emitted only when `include_transitive: true`. One extra
  backend round-trip, in exchange for a contract that matches what the
  description says.

- **Hard-error on silent no-op flag combinations.** Three inputs used
  to be silently ignored when paired incorrectly; each now returns
  `INVALID_ARGUMENT`:
  - MCP `max_depth` without `include_transitive`
  - MCP `include_importers` without `include_transitive`
  - CLI `--depth` without `--transitive`

  On an agent-facing tool, silently-ignored inputs are worse than a
  rejection: the caller believes they got the effect they asked for.

- **Stale documentation from the UX-redesign rounds.**
  - `tools.md` tool-table row still claimed `package_dependencies`
    returned "an opaque DAG" — we dropped `transitive.dag` from the
    envelope mid-iteration.
  - `tools.md` MCP-schema note still said "CLI's `--depth` defaults to
    3"; `--depth` has no default, backend full-graph traversal wins.
  - `cli-commands.md` two-views section mentioned NuGet TFMs in the
    `pkg deps` context, but NuGet is rejected client-side for this
    tool.

## Cleanup

Envelope builder (`src/shared/package-dependencies-response.ts`) had
~155 lines of dead code left behind after the envelope took over
provenance decoding from the terminal formatter: `DecodedConflict`,
`decodeConflictEntry`, `decodeCycleEntry`, `ProvenanceEntry`,
`buildProvenanceLookup`. Deleted. Module header rewritten to describe
the current state (preprocessed transitive, typed conflicts/cycles
with raw fallback, no `transitive.dag`, no `uniqueDependencies`).

## Tests

- `bun test` — 962 pass, 0 fail (was 958; added 4 new tests).
- `bun run typecheck` / `bun run build` clean.
- Three new tests:
  - MCP: `runtime.items[].version` resolves even when
    `include_transitive` is absent (locks the wire behaviour).
  - MCP: `max_depth` without `include_transitive` returns
    `INVALID_ARGUMENT`.
  - MCP: `include_importers` without `include_transitive` returns
    `INVALID_ARGUMENT`.
  - CLI: `--depth` without `--transitive` errors with the expected
    hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jlitola
jlitola merged commit 6ee8600 into main Apr 20, 2026
3 checks passed
jlitola added a commit that referenced this pull request Apr 20, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary`
(#13), `package_vulnerabilities` (#14), `package_dependencies` (#16
+ follow-up #17). Behind the existing `code_navigation` capability
gate. Both surfaces share a single request builder, envelope builder,
and error classifier; a parity test asserts `toEqual` JSON across
the two surfaces for every service-sourced fixture.

## Surfaces

- **`githits pkg changelog [spec]`** — default latest-mode output is
  a summary row (identity · source · mode · entry count) plus a
  one-line `version  date  url` per entry, newest-first. `--from
  <v>` switches to range mode (all entries between `--from` and
  `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>`
  shape latest mode. `--verbose` expands each entry with its full
  markdown body, indented and dimmed. `--no-body` drops body fields
  from both terminal and JSON. `--git-ref` targets a specific
  branch/tag for CHANGELOG.md source. `--repo-url <url>` is an
  alternative addressing mode (mutually exclusive with `<spec>`).
- **`package_changelog`** — MCP tool with the same envelope. Dual
  addressing: `registry` + `package_name` XOR `repo_url`. Permissive
  Zod schema (in-handler validation). `include_bodies` (default
  true) mirrors CLI's `--no-body`. Description is self-contained —
  no CLI-flag references.

## Design choices

- **Dual addressing is unique to this tool.** P1 / P2 / P3 all
  accept only `registry` + `package_name` because their underlying
  backend queries are registry-metadata APIs. `packageChangelog` is
  intrinsically repo-level (its sources are GitHub Releases,
  CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in
  the GraphQL signature, not a bolt-on. Exposing it on MCP was
  non-negotiable: `packageSummary` cannot resolve a repo URL → spec,
  so agents starting from a repo URL had no path in. Documented in
  `tools.md` so future tool authors don't cargo-cult the asymmetry.
- **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat
  `@version` as "for this exact version"; changelog has no single-
  version query. Silently remapping to `toVersion` would be a
  client-invented semantic shift. Rejected client-side with
  `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`.
- **Data-first envelope.** `{registry|repoUrl, source, mode,
  entries: {count, items}, filter?}`. `source` always present here
  (null-source case is promoted to `NOT_FOUND` at the service
  boundary and never reaches the envelope). `mode` derived from
  request — `"range"` iff `fromVersion` non-null after
  normalisation, `"latest"` otherwise. `entries.count` computed
  client-side from `items.length`; the backend's count isn't
  selected on the wire so the invariant holds by construction.
- **`version` kept when null, other per-entry nullables stripped.**
  `version` is the primary key agents index by, so the slot is
  always present (possibly null); stripping other nullables keeps
  the envelope lean. `body` additionally stripped under
  `include_bodies: false`.
- **`filter.*` echo tracks explicit fields only.** Request builder
  tracks `explicitFilterFields` set; envelope emits `filter.*` only
  for caller-supplied inputs. Backend defaults (`limit: 10`,
  `toVersion: <latest>`) never round-trip as caller intent.
- **`include_bodies` lever.** Release bodies on large packages can
  run 10 KB+ per entry; `include_bodies: false` drops `body` from
  all items explicitly (not silent truncation). Other fields
  preserved so agents still get the version / date / URL timeline.
- **`metadata` dropped from envelope in v1.** Source-specific opaque
  `GenericJSON`; revisit via agent feedback with a
  `TODO(pkgseer-backend)` anchor if demand surfaces.
- **No client-side registry restriction.** Unlike P3's 6-registry
  client-side gate, `packageChangelog` is source-pull rather than
  registry-query; the backend returns `NOT_FOUND` for registries
  without changelog sources. Live-smoke matrix documented in the
  plan to verify behaviour per registry.
- **Mode mutual exclusion enforced client-side.** `--from` /
  `from_version` + `--limit` / `limit` together → `INVALID_ARGUMENT`
  with actionable hint.
- **`source: null` promoted to NOT_FOUND.** Typed
  `PackageIntelligenceChangelogSourceNotFoundError` at the service
  boundary; shared classifier routes to `NOT_FOUND` with a message
  naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs).
  Empty `entries.items: []` with a valid `source` is success, not
  error.
- **`--verbose` vs `--no-body` vs `--json` interaction.** `--verbose`
  is terminal-only (expands markdown bodies per entry); does not
  change `--json`. `--no-body` affects both terminal and `--json`.
  `--json` output shape is independent of `--verbose`.
- **Shared `promoteGenericVersionNotFound` extended.** The helper
  now recognises `fromVersion` / `toVersion` in addition to
  `version`; preference order `version → fromVersion → toVersion`.
  `registry` / `packageName` made optional so repo-URL-addressed
  requests flow through without a spec. P2 / P3 regressions guarded
  by the existing tests plus 4 new helper tests.

## Tests

- `bun test` — 1053 pass, 0 fail (was 966; +87 new tests).
- `bun run typecheck` / `bun run build` / `bun run lint` — clean.
- Unit tests: request-builder (addressing XOR, `<spec>@<version>`
  rejection, `--from`/`--limit` mutex, limit bounds, tag-style
  rejection, pre-release versions on `--from`/`--to`, explicit-flag
  tracking), envelope builder (shape, null handling, `include_bodies`,
  mode derivation, filter echo, empty entries), terminal formatter
  (default one-liners, `--verbose` expansion, missing fields), CLI
  action (13 cases incl. addressing, errors, flag interactions),
  MCP tool (15 cases incl. validation + service errors).
- Parity test: 12 fixtures (happy latest, range mode, repo-URL
  addressing, `--no-body` / `include_bodies: false`, default bodies,
  empty entries, `NOT_FOUND` no-source, `TargetNotFoundError`,
  `VERSION_NOT_FOUND` with structured details, `BACKEND_ERROR`,
  `INVALID_ARGUMENT` for `<spec>@<version>` and `--from`+`--limit`).
- Live-smoke deferred to review cycle (local non-interactive bash
  keychain prompt blocks the CLI startup handshake; shape correctness
  is locked in via the verbatim schema Zod parse + parity test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola added a commit that referenced this pull request Apr 21, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary`
(#13), `package_vulnerabilities` (#14), `package_dependencies` (#16
+ follow-up #17). Behind the existing `code_navigation` capability
gate. Both surfaces share a single request builder, envelope builder,
and error classifier; a parity test asserts `toEqual` JSON across
the two surfaces for every service-sourced fixture.

## Surfaces

- **`githits pkg changelog [spec]`** — default latest-mode output is
  a summary row (identity · source · mode · entry count) plus a
  one-line `version  date  url` per entry, newest-first. `--from
  <v>` switches to range mode (all entries between `--from` and
  `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>`
  shape latest mode. `--verbose` expands each entry with its full
  markdown body, indented and dimmed. `--no-body` drops body fields
  from both terminal and JSON. `--git-ref` targets a specific
  branch/tag for CHANGELOG.md source. `--repo-url <url>` is an
  alternative addressing mode (mutually exclusive with `<spec>`).
- **`package_changelog`** — MCP tool with the same envelope. Dual
  addressing: `registry` + `package_name` XOR `repo_url`. Permissive
  Zod schema (in-handler validation). `include_bodies` (default
  true) mirrors CLI's `--no-body`. Description is self-contained —
  no CLI-flag references.

## Design choices

- **Dual addressing is unique to this tool.** P1 / P2 / P3 all
  accept only `registry` + `package_name` because their underlying
  backend queries are registry-metadata APIs. `packageChangelog` is
  intrinsically repo-level (its sources are GitHub Releases,
  CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in
  the GraphQL signature, not a bolt-on. Exposing it on MCP was
  non-negotiable: `packageSummary` cannot resolve a repo URL → spec,
  so agents starting from a repo URL had no path in. Documented in
  `tools.md` so future tool authors don't cargo-cult the asymmetry.
- **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat
  `@version` as "for this exact version"; changelog has no single-
  version query. Silently remapping to `toVersion` would be a
  client-invented semantic shift. Rejected client-side with
  `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`.
- **Data-first envelope.** `{registry|repoUrl, source, mode,
  entries: {count, items}, filter?}`. `source` always present here
  (null-source case is promoted to `NOT_FOUND` at the service
  boundary and never reaches the envelope). `mode` derived from
  request — `"range"` iff `fromVersion` non-null after
  normalisation, `"latest"` otherwise. `entries.count` computed
  client-side from `items.length`; the backend's count isn't
  selected on the wire so the invariant holds by construction.
- **`version` kept when null, other per-entry nullables stripped.**
  `version` is the primary key agents index by, so the slot is
  always present (possibly null); stripping other nullables keeps
  the envelope lean. `body` additionally stripped under
  `include_bodies: false`.
- **`filter.*` echo tracks explicit fields only.** Request builder
  tracks `explicitFilterFields` set; envelope emits `filter.*` only
  for caller-supplied inputs. Backend defaults (`limit: 10`,
  `toVersion: <latest>`) never round-trip as caller intent.
- **`include_bodies` lever.** Release bodies on large packages can
  run 10 KB+ per entry; `include_bodies: false` drops `body` from
  all items explicitly (not silent truncation). Other fields
  preserved so agents still get the version / date / URL timeline.
- **`metadata` dropped from envelope in v1.** Source-specific opaque
  `GenericJSON`; revisit via agent feedback with a
  `TODO(pkgseer-backend)` anchor if demand surfaces.
- **No client-side registry restriction.** Unlike P3's 6-registry
  client-side gate, `packageChangelog` is source-pull rather than
  registry-query; the backend returns `NOT_FOUND` for registries
  without changelog sources. Live-smoke matrix documented in the
  plan to verify behaviour per registry.
- **Mode mutual exclusion enforced client-side.** `--from` /
  `from_version` + `--limit` / `limit` together → `INVALID_ARGUMENT`
  with actionable hint.
- **`source: null` promoted to NOT_FOUND.** Typed
  `PackageIntelligenceChangelogSourceNotFoundError` at the service
  boundary; shared classifier routes to `NOT_FOUND` with a message
  naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs).
  Empty `entries.items: []` with a valid `source` is success, not
  error.
- **`--verbose` vs `--no-body` vs `--json` interaction.** `--verbose`
  is terminal-only (expands markdown bodies per entry); does not
  change `--json`. `--no-body` affects both terminal and `--json`.
  `--json` output shape is independent of `--verbose`.
- **Shared `promoteGenericVersionNotFound` extended.** The helper
  now recognises `fromVersion` / `toVersion` in addition to
  `version`; preference order `version → fromVersion → toVersion`.
  `registry` / `packageName` made optional so repo-URL-addressed
  requests flow through without a spec. P2 / P3 regressions guarded
  by the existing tests plus 4 new helper tests.

## Tests

- `bun test` — 1053 pass, 0 fail (was 966; +87 new tests).
- `bun run typecheck` / `bun run build` / `bun run lint` — clean.
- Unit tests: request-builder (addressing XOR, `<spec>@<version>`
  rejection, `--from`/`--limit` mutex, limit bounds, tag-style
  rejection, pre-release versions on `--from`/`--to`, explicit-flag
  tracking), envelope builder (shape, null handling, `include_bodies`,
  mode derivation, filter echo, empty entries), terminal formatter
  (default one-liners, `--verbose` expansion, missing fields), CLI
  action (13 cases incl. addressing, errors, flag interactions),
  MCP tool (15 cases incl. validation + service errors).
- Parity test: 12 fixtures (happy latest, range mode, repo-URL
  addressing, `--no-body` / `include_bodies: false`, default bodies,
  empty entries, `NOT_FOUND` no-source, `TargetNotFoundError`,
  `VERSION_NOT_FOUND` with structured details, `BACKEND_ERROR`,
  `INVALID_ARGUMENT` for `<spec>@<version>` and `--from`+`--limit`).
- Live-smoke deferred to review cycle (local non-interactive bash
  keychain prompt blocks the CLI startup handshake; shape correctness
  is locked in via the verbatim schema Zod parse + parity test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola added a commit that referenced this pull request Apr 21, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary`
(#13), `package_vulnerabilities` (#14), `package_dependencies` (#16
+ follow-up #17). Behind the existing `code_navigation` capability
gate. Both surfaces share a single request builder, envelope builder,
and error classifier; a parity test asserts `toEqual` JSON across
the two surfaces for every service-sourced fixture.

## Surfaces

- **`githits pkg changelog [spec]`** — default latest-mode output is
  a summary row (identity · source · mode · entry count) plus, per
  entry, a `version  date  url` header followed by the first 10
  lines of the markdown body (with a `… (+N more lines — use
  --verbose for the full body)` footer when the body is longer).
  `--verbose` uncaps the body preview; `--no-body` drops bodies
  entirely from both terminal and `--json`. `--from <v>` switches
  to range mode (all entries between `--from` and `--to`/latest;
  `--limit` rejected). `--to <v>` / `--limit <n>` shape latest
  mode. `--git-ref` targets a branch/tag for CHANGELOG.md source.
  `--repo-url <url>` is an alternative addressing mode, mutually
  exclusive with `<spec>`.
- **`package_changelog`** — MCP tool with the same envelope. Dual
  addressing: `registry` + `package_name` XOR `repo_url`.
  Permissive Zod schema + in-handler validation. `include_bodies`
  (default true) mirrors CLI's `--no-body` and controls JSON
  envelope bodies.

## Design choices

- **Dual addressing is unique to this tool.** P1 / P2 / P3 all
  accept only `registry` + `package_name` because their underlying
  backend queries are registry-metadata APIs. `packageChangelog`
  is intrinsically repo-level (sources: GitHub Releases,
  CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode
  in the GraphQL signature. Exposing it on MCP was non-negotiable:
  `packageSummary` cannot resolve repo-URL → spec, so agents
  starting from a repo URL had no path in. Documented in
  `tools.md` so future tool authors don't cargo-cult the
  asymmetry.
- **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat
  `@version` as "for this exact version"; changelog has no
  single-version query. Silently remapping to `toVersion` would be
  a client-invented semantic shift. Rejected with
  `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`.
- **Body preview capped at 10 lines by default.** Release bodies
  routinely run 50-100+ lines — showing them unbounded would
  swamp the terminal; showing none would make the whole command
  useless for answering "what changed". 10-line cap shows the
  first one or two sections plus preamble, which is usually
  enough to answer the question; `--verbose` lifts the cap when
  full context is needed; `--no-body` drops bodies entirely for
  a pure timeline view. JSON envelope is always full-body unless
  `--no-body` / `include_bodies: false` is set — `--verbose` is
  terminal-only.
- **Data-first envelope.** `{registry|repoUrl, source, mode,
  entries: {count, items}, filter?}`. `source` always present
  here (null-source case promoted to `NOT_FOUND` at the service
  boundary; envelope builder has a defence-in-depth throw if the
  invariant is ever violated upstream). `mode` derived from
  request — `"range"` iff `fromVersion` non-null, `"latest"`
  otherwise. `entries.count` computed client-side from
  `items.length`; backend count not selected on the wire.
- **`version` kept when null, other per-entry nullables stripped.**
  `version` is the primary key agents index by. Empty-string
  values (e.g. `body: ""`) are preserved — distinct from absent
  — so agents can tell "empty release notes" from "no notes
  field". Terminal formatter renders `(empty release notes)`
  sentinel for empty-string bodies.
- **`filter.*` tracks explicit fields only.** Builder returns an
  `explicitFilterFields` set; envelope emits `filter.*` only for
  caller-supplied inputs. Backend defaults (`limit: 10`,
  `toVersion: latest`) never round-trip as caller intent.
- **`include_bodies` lever.** Default true. `false` drops `body`
  from every item explicitly; other fields preserved so agents
  still get the version / date / URL timeline. Measured 5.13×
  envelope size reduction on a 20-entry `npm:typescript` request
  (17.3 KB → 3.4 KB).
- **`--no-body` + `--verbose` is an error.** Contradictory
  intents; CLI rejects with an actionable hint rather than
  silently ignoring `--verbose`.
- **MCP `limit` validation in the shared builder, not Zod.** If
  Zod hard-rejected `limit: 51` at the SDK level, the agent would
  see a raw SDK error instead of our shared
  `{error, code, retryable}` envelope. Schema is permissive;
  builder enforces bounds.
- **Registry coverage — all 9 on the wire, no client-side
  restriction.** `packageChangelog` is source-pull, not
  registry-query. Live-smoke confirmed npm / pypi / hex / crates
  / vcpkg / maven / packagist return useful data; nuget returns a
  generic `BACKEND_ERROR`; zig packages weren't in the backend
  index. All paths produce the shared envelope correctly.
- **Mode mutex enforced client-side.** `--from` / `from_version`
  + `--limit` / `limit` → `INVALID_ARGUMENT` with actionable hint.
- **`metadata` dropped in v1.** Source-specific opaque
  `GenericJSON`; revisit via agent feedback.
- **Shared `promoteGenericVersionNotFound` extended.** Now
  recognises `fromVersion` / `toVersion` in addition to
  `version`. `registry` / `packageName` made optional so
  repo-URL-addressed requests flow through. Preference order
  `version → fromVersion → toVersion`. P2 / P3 regressions
  guarded by existing tests + 5 new helper tests.

## Tests

- `bun test` — 1062 pass, 0 fail.
- `bun run typecheck` / `bun run build` / `bun run lint` — clean.
- Request builder, envelope builder, terminal formatter, MCP tool,
  CLI action, parity fixtures (12) — see docs for full matrix.
- Live-smoke against production pkgseer: latest / range /
  repo-URL / `--no-body` / error paths / registry matrix /
  body-preview cap / `--verbose` uncap / truncation footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola added a commit that referenced this pull request Apr 21, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary`
(#13), `package_vulnerabilities` (#14), `package_dependencies` (#16
+ follow-up #17). Behind the existing `code_navigation` capability
gate. Both surfaces share a single request builder, envelope builder,
and error classifier; a parity test asserts `toEqual` JSON across
the two surfaces for every service-sourced fixture.

## Surfaces

- **`githits pkg changelog [spec]`** — default latest-mode output is
  a summary row (identity · source · mode · entry count) plus, per
  entry, a `version  date  url` header followed by the first 10
  lines of the markdown body (with a `… (+N more lines — use
  --verbose for the full body)` footer when the body is longer).
  `--verbose` uncaps the body preview; `--no-body` drops bodies
  entirely from both terminal and `--json`. `--from <v>` switches
  to range mode (all entries between `--from` and `--to`/latest;
  `--limit` rejected). `--to <v>` / `--limit <n>` shape latest
  mode. `--git-ref` targets a branch/tag for CHANGELOG.md source.
  `--repo-url <url>` is an alternative addressing mode, mutually
  exclusive with `<spec>`.
- **`package_changelog`** — MCP tool with the same envelope. Dual
  addressing: `registry` + `package_name` XOR `repo_url`.
  Permissive Zod schema + in-handler validation. `include_bodies`
  (default true) mirrors CLI's `--no-body` and controls JSON
  envelope bodies.

## Design choices

- **Dual addressing is unique to this tool.** P1 / P2 / P3 all
  accept only `registry` + `package_name` because their underlying
  backend queries are registry-metadata APIs. `packageChangelog`
  is intrinsically repo-level (sources: GitHub Releases,
  CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode
  in the GraphQL signature. Exposing it on MCP was non-negotiable:
  `packageSummary` cannot resolve repo-URL → spec, so agents
  starting from a repo URL had no path in. Documented in
  `tools.md` so future tool authors don't cargo-cult the
  asymmetry.
- **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat
  `@version` as "for this exact version"; changelog has no
  single-version query. Silently remapping to `toVersion` would be
  a client-invented semantic shift. Rejected with
  `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`.
- **Body preview capped at 10 lines by default.** Release bodies
  routinely run 50-100+ lines — showing them unbounded would
  swamp the terminal; showing none would make the whole command
  useless for answering "what changed". 10-line cap shows the
  first one or two sections plus preamble, which is usually
  enough to answer the question; `--verbose` lifts the cap when
  full context is needed; `--no-body` drops bodies entirely for
  a pure timeline view. JSON envelope is always full-body unless
  `--no-body` / `include_bodies: false` is set — `--verbose` is
  terminal-only.
- **Data-first envelope.** `{registry|repoUrl, source, mode,
  entries: {count, items}, filter?}`. `source` always present
  here (null-source case promoted to `NOT_FOUND` at the service
  boundary; envelope builder has a defence-in-depth throw if the
  invariant is ever violated upstream). `mode` derived from
  request — `"range"` iff `fromVersion` non-null, `"latest"`
  otherwise. `entries.count` computed client-side from
  `items.length`; backend count not selected on the wire.
- **`version` kept when null, other per-entry nullables stripped.**
  `version` is the primary key agents index by. Empty-string
  values (e.g. `body: ""`) are preserved — distinct from absent
  — so agents can tell "empty release notes" from "no notes
  field". Terminal formatter renders `(empty release notes)`
  sentinel for empty-string bodies.
- **`filter.*` tracks explicit fields only.** Builder returns an
  `explicitFilterFields` set; envelope emits `filter.*` only for
  caller-supplied inputs. Backend defaults (`limit: 10`,
  `toVersion: latest`) never round-trip as caller intent.
- **`include_bodies` lever.** Default true. `false` drops `body`
  from every item explicitly; other fields preserved so agents
  still get the version / date / URL timeline. Measured 5.13×
  envelope size reduction on a 20-entry `npm:typescript` request
  (17.3 KB → 3.4 KB).
- **`--no-body` + `--verbose` is an error.** Contradictory
  intents; CLI rejects with an actionable hint rather than
  silently ignoring `--verbose`.
- **MCP `limit` validation in the shared builder, not Zod.** If
  Zod hard-rejected `limit: 51` at the SDK level, the agent would
  see a raw SDK error instead of our shared
  `{error, code, retryable}` envelope. Schema is permissive;
  builder enforces bounds.
- **Registry coverage — all 9 on the wire, no client-side
  restriction.** `packageChangelog` is source-pull, not
  registry-query. Live-smoke confirmed npm / pypi / hex / crates
  / vcpkg / maven / packagist return useful data; nuget returns a
  generic `BACKEND_ERROR`; zig packages weren't in the backend
  index. All paths produce the shared envelope correctly.
- **Mode mutex enforced client-side.** `--from` / `from_version`
  + `--limit` / `limit` → `INVALID_ARGUMENT` with actionable hint.
- **`metadata` dropped in v1.** Source-specific opaque
  `GenericJSON`; revisit via agent feedback.
- **Shared `promoteGenericVersionNotFound` extended.** Now
  recognises `fromVersion` / `toVersion` in addition to
  `version`. `registry` / `packageName` made optional so
  repo-URL-addressed requests flow through. Preference order
  `version → fromVersion → toVersion`. P2 / P3 regressions
  guarded by existing tests + 5 new helper tests.

## Tests

- `bun test` — 1062 pass, 0 fail.
- `bun run typecheck` / `bun run build` / `bun run lint` — clean.
- Request builder, envelope builder, terminal formatter, MCP tool,
  CLI action, parity fixtures (12) — see docs for full matrix.
- Live-smoke against production pkgseer: latest / range /
  repo-URL / `--no-body` / error paths / registry matrix /
  body-preview cap / `--verbose` uncap / truncation footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant