Skip to content

feat(pkg-intel): add package_dependencies (pkg deps + MCP) - #16

Merged
jlitola merged 1 commit into
mainfrom
feat/pkg-intel-dependencies
Apr 20, 2026
Merged

feat(pkg-intel): add package_dependencies (pkg deps + MCP)#16
jlitola merged 1 commit into
mainfrom
feat/pkg-intel-dependencies

Conversation

@jlitola

@jlitola jlitola commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Third Wave 1 package-intelligence tool, following package_summary (#13) and package_vulnerabilities (#14). Behind the existing code_navigation capability gate.

  • githits pkg deps <spec> — default shows a summary row (counts + hidden-groups mentioned by name) plus the direct-runtime dependency list as name@resolved-version. --transitive replaces the deps list with the full unique transitive closure (alphabetical, one per line, with conflict / cycle counts on the summary). In both modes, --verbose expands each entry to a multi-line block: name@version followed by indented - <constraint> required by importer@version, … bullets, with importers sharing a constraint collapsed onto one bullet. --verbose also expands summary-level conflicts and circular dependencies into structured blocks. --groups / --lifecycle adds a structured groups block beneath the deps list (additive, not a replacement view). Supports @version, uppercase-tolerated --lifecycle, --depth 1–10 (no default — backend's full-graph traversal), --json.
  • package_dependencies — MCP tool with the same envelope. Permissive Zod schema (lifecycle accepts string or string array); opt-in include_transitive, include_importers (requires include_transitive), max_depth (requires include_transitive). Descriptions are self-contained (no CLI-flag references) so agents can use the tool without extra context.

Design choices

  • Data-first envelope. runtime, groups, transitive, and filter are independent blocks emitted based on what the backend returned and what the caller asked for, not on caller flags. runtime emits whenever the backend returned direct[] (including zero-dep packages); groups emits whenever the backend returned dependencyGroups, distinguishing {items: []} (filter matched nothing) from absent (backend has no groups concept). Dependency lists everywhere use items for a single consistent key.
  • Preprocessed transitive block. The DAG decoder lives in the envelope builder: transitive.packages[] ships as {name, version} records, and include_importers layers on per-package importers[] with importer name / version / constraint. Raw backend DAG is deliberately dropped from this PR's envelope — a future pkg deps-dag command will expose a typed graph surface under a maintained contract. transitive.conflicts[] and transitive.circularDependencies[] are typed ({name, requiredVersions} / {cycle: string[]}) when the observed backend shape decodes; raw passthrough otherwise.
  • Envelope-size control. include_importers is opt-in because adding provenance roughly quadruples payload size on heavy graphs (≈59 KB → 14 KB on jest without it). CLI's --verbose wires through to enable it in both terminal output and --json — agents reading CLI JSON get the same lean-vs-rich toggle.
  • Runtime version resolution. Both surfaces always fetch the transitive DAG on the wire (depth 1 when the caller didn't request transitive) so every direct dep renders as name@resolved-version alongside its constraint — matching what the package will actually install. Costs one extra backend round-trip vs direct-only; intentional for UX.
  • No silent no-op flags. include_importers without include_transitive, max_depth without include_transitive, and CLI --depth without --transitive are all rejected with INVALID_ARGUMENT rather than silently ignored. Each case has an explicit test.
  • Ecosystem-aware vocabulary. PyPI feature-typed groups render as name (optional, extra) (PEP 508 terminology); Cargo keeps feature.
  • Terminal-only dedup. Crates target-cfg branches emit duplicate {name, constraint} tuples; the terminal formatter collapses them for scannability, the JSON envelope preserves every tuple. A parity fixture exercises the round-trip.
  • No include_groups MCP input. The data-first envelope makes it a silently-ignored no-op; omitting it forces the correct mental model.
  • Canonical versions only. Tag-style v-prefixed inputs rejected client-side with INVALID_ARGUMENT before the backend call (carries over from P2).
  • Registry coverage. Only npm, PyPI, Hex, Crates, vcpkg, and Zig support the packageDependencies query. Other registries are rejected client-side with a tool-specific message.
  • Shared promoteGenericVersionNotFound helper. Extracted from P2's inline helper and now used by both packageVulnerabilities and packageDependencies executors.
  • Remaining untyped passthroughs. transitive.conflicts, transitive.circularDependencies (when decoding fails), and groups.environmentConstraints flow through as opaque UntypedGenericJSON. Each field has a TODO(pkgseer-backend) anchor flagging the upgrade path when concrete types land upstream.

CLI UX details

  • Summary row always shows counts + hidden-groups-by-name (e.g. Hidden groups: development — use --groups.).
  • --lifecycle implies --groups — no flat projection for non-runtime lifecycles on the wire.
  • Runtime + group dependency lists sort alphabetically client-side regardless of backend order.
  • --depth validates exact integers; partial inputs like 3.5 or 5abc are rejected rather than silently truncated.
  • VERSION_NOT_FOUND error enriched with package / requested / available detail lines (reuses P2's helper).

Tests

  • bun test — 962 pass, 0 fail.
  • bun run typecheck / bun run build / bun run lint — clean.
  • Parity fixtures cover: happy runtime, zero-dep (omits groups), full-view, optional-lifecycle (tokio features), multi-lifecycle, filter-matched-nothing (groups: {items: []}), Crates-target-cfg dedup round-trip, transitive with importers, lean-default transitive (no importers), versioned match / real-diff, NOT_FOUND, VERSION_NOT_FOUND with structured details, BACKEND_ERROR, three INVALID_ARGUMENT cases.
  • Live-verified against production pkgseer across npm / PyPI / Crates / Hex / vcpkg, including importer provenance on real express data and conflict detection on jest (16 conflicts).

Test plan

  • bun test passes (962 / 0)
  • bun run typecheck clean
  • bun run build clean
  • bun run lint clean
  • pkg deps scenarios smoked against a token with code_navigation capability
  • MCP tools/list shows package_dependencies under an open-gate token; server-level instructions include the bullet
  • pkg deps --help renders under an open-gate token

@jlitola
jlitola force-pushed the feat/pkg-intel-dependencies branch 6 times, most recently from 1d369e8 to ee9626b Compare April 20, 2026 18:58
Adds the `package_dependencies` MCP tool and `githits pkg deps` CLI
command 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 deps <spec>`** — default shows a summary row
  (counts + hidden-groups mention by name) plus the flat direct-
  runtime dependency list. `--transitive` replaces the deps list
  with the full unique transitive closure (alphabetical, one per
  line). `--transitive --verbose` annotates each entry with
  `(required by <importer>@<constraint>, …)` computed from the
  DAG. `--groups` / `--lifecycle` adds a structured groups block
  beneath the deps list (additive, not a replacement view).
  Supports `@version`, uppercase-tolerated `--lifecycle`, `--depth`
  1–10 (CLI default 3), `--verbose`, `--json`.
- **`package_dependencies`** — MCP tool with the same lean
  envelope. Permissive Zod schema (`lifecycle` accepts string or
  string array); no `max_depth` default on the MCP path so the
  backend's full-graph default applies.

## Design choices

- **Data-first envelope.** `runtime`, `groups`, `transitive`, and
  `filter` are independent blocks emitted based on what the
  backend returned and what the caller asked for, not on caller
  flags. `runtime` emits whenever the backend returned `direct[]`
  (including zero-dep packages); `groups` emits whenever the
  backend returned `dependencyGroups`, distinguishing `{items: []}`
  (filter matched nothing) from absent (backend has no groups
  concept). Dependency lists everywhere use `items` for a single
  consistent key.
- **Ergonomic semantic model.** Summary row always leads with the
  scope signal; `--transitive` replaces (not augments) the deps
  list; groups view is a separate block composing beneath; hidden
  groups surface by name in the summary (`Hidden groups: argon2,
  bcrypt — use --groups.`) rather than as an aggregate footer.
- **Best-effort DAG decoder** lives in the formatter (not the
  service) so the JSON envelope stays fully opaque for
  `transitive.dag`. Handles `[registry, name, version]` tuples +
  `{n, v, l}` objects on the node side; `[from, to, constraint?,
  lifecycle?]` + object form on edges. Returns null on unknown
  shapes — provenance silently degrades but the transitive list
  still renders.
- **Terminal-only dedup.** Crates target-cfg branches emit
  duplicate `{name, constraint}` tuples; the terminal formatter
  collapses them for scannability, the JSON envelope preserves
  every tuple. A parity fixture exercises the round-trip.
- **Ecosystem-aware vocabulary.** PyPI feature-typed groups
  render as `name (optional, extra)` (PEP 508); Cargo keeps
  `feature`.
- **No `include_groups` MCP input.** The data-first envelope
  makes it a silently-ignored no-op; omitting it forces the
  correct mental model.
- **Canonical versions only.** Tag-style `v`-prefixed inputs
  rejected client-side with `INVALID_ARGUMENT`.
- **Registry coverage.** npm, PyPI, Hex, Crates, vcpkg, Zig.
  Other registries rejected client-side with a tool-specific
  message.
- **Shared `promoteGenericVersionNotFound` helper.** Extracted
  from P2's inline helper; used by both `packageVulnerabilities`
  and `packageDependencies` executors.
- **Untyped passthroughs** (`transitive.dag`,
  `transitive.conflicts`, `transitive.circularDependencies`,
  `groups.environmentConstraints`) flow through as opaque
  `UntypedGenericJSON`. Backend declares them `GenericJSON` and
  hasn't published concrete shapes; typing them client-side would
  be speculative. Each field has a `TODO(pkgseer-backend)` anchor
  flagging the upgrade path when concrete types land upstream.

## CLI UX details

- Summary row always shows counts + hidden-groups-by-name.
- `--transitive` replaces the deps list with the full unique
  transitive closure; `--verbose` adds `(required by …)`
  provenance per entry.
- Alphabetical sort on every list is explicit and tested.
- `--depth` validates exact integers; partial inputs like `3.5`
  or `5abc` are rejected rather than silently truncated.
- `VERSION_NOT_FOUND` error enriched with `package` / `requested`
  / `available` detail lines (reuses P2's helper).

## Tests

- `bun test` — 951 pass, 0 fail.
- `bun run typecheck` / `bun run build` / `bun run lint` — clean.
- Parity fixtures (16) cover: happy flat-runtime, zero-dep,
  full-view, optional-lifecycle (tokio features), multi-lifecycle,
  filter-matched-nothing, Crates-target-cfg dedup round-trip,
  transitive with DAG passthrough, versioned match / real-diff,
  `NOT_FOUND`, `VERSION_NOT_FOUND` with structured details,
  `BACKEND_ERROR`, and three `INVALID_ARGUMENT` cases.
- Live-verified against production pkgseer across npm / PyPI /
  Crates / Hex / vcpkg, with DAG provenance working on real
  express data showing importer→constraint relationships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jlitola
jlitola force-pushed the feat/pkg-intel-dependencies branch from ee9626b to 746082c Compare April 20, 2026 19:07
@jlitola
jlitola merged commit 81bd8ec 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