Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ deployed.

GitHits works with package and repository targets such as:

- package specs: `npm:react`, `npm:react@18.2.0`, `pypi:requests`, `crates:serde`
- package specs: `npm:react`, `npm:react@18.2.0`, `pypi:requests`, `crates:serde`, `pub:path`
- GitHub repos: `https://github.com/expressjs/express`, `github:expressjs/express@main`

Package inspection supports npm, PyPI, Hex, Crates, NuGet, Maven, Packagist,
RubyGems, Go, Swift, vcpkg, and Zig. Advisory data is unavailable for vcpkg and
RubyGems, Go, Swift, Pub, vcpkg, and Zig. Advisory data is unavailable for vcpkg and
Zig; dependency graph support varies by registry. `cargo:` is an input alias for
`crates:` (public crates.io); canonical registry identities remain `crates`.

Expand Down
6 changes: 6 additions & 0 deletions changes/dependency-source-namespaces.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"githits": patch
"@githits/mcp": patch
---

- **Dependency source identity** - Preserve graph registry namespaces in transitive package and importer JSON, distinguish SDK sources in text, and display MCP importer provenance when requested.
6 changes: 6 additions & 0 deletions changes/pub-registry-admission.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"githits": minor
"@githits/mcp": minor
---

- **Pub package addresses** - Accept `pub:` package targets across CLI and MCP package analysis, resolution, and source/docs requests while preserving backend evidence and errors. The package Agent Skill lists Pub registry guidance with this release.
18 changes: 9 additions & 9 deletions docs/implementation/cli-commands.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion docs/implementation/mcp-cli-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,11 @@ When a new tool lands with both MCP and CLI surfaces:
terminal formatter collapses them; the JSON envelope preserves
every duplicate the backend emitted.
- **Preprocessed transitive.** `transitive.packages[]` carries
`{name, version, importers[]}` records; `conflicts[]` is typed
`{registry?, name, version?, importers?}` records. Exact unambiguous graph
matches retain registry independently of importer output; absent or ambiguous
matches omit registry/provenance. Importers retain registry/name/version/constraint.
Text qualifies non-header namespaces and MCP `include_importers: true` displays
provenance while keeping issue summaries compact. `conflicts[]` is typed
`{name, requiredVersions, requirements}` when decodable;
`circularDependencies[]` is typed `{cycle: string[]}` when decodable.
Each conflict requirement preserves one backend edge in its original
Expand Down
38 changes: 34 additions & 4 deletions docs/implementation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,31 @@ performs one sequential dependency query with the resolved root version and
fails closed on malformed identity/count/fix evidence instead of returning
partial direct-only results.

## Pub package addresses

Pub uses the shared `pub` surface registry and `PUB` backend enum for package
analysis, target resolution, and source/documentation addressing. The core registry
taxonomy owns this conversion; the separate MCP capability matrix includes Pub for
vulnerability queries. Neither surface fetches pub.dev or selects dependency versions.

Package names retain native case and exact version strings retain build metadata.
`pkg info pub:path` remains latest-only. Download counts are package-wide observations:
zero is known zero and absent counts remain unknown. Dependency graphs, advisory facts,
release evidence, unsupported-context errors, and preparation outcomes remain backend
facts. Public package queries do not establish an uploaded Dart project's graph or a
Flutter SDK model. Source/docs addressing uses existing repository preparation and
returned locators; it does not create a generated Dartdoc corpus.

The public package Agent Skill tracks the published CLI. During release preparation,
add Pub to its dependency registry list and the info/vulnerability/dependency lists
in `skills/githits-package/references/package.md`, including a native-case/build
metadata example. This keeps skills.sh guidance aligned with the released artifact.

## Ecosystem Audit

Use `bun run audit:pkg-ecosystems` to run a live CLI audit across representative packages from every package registry supported by package metadata tools. The script checks `pkg_info`, `pkg_changelog`, `pkg_vulns`, and `pkg_deps` with JSON output so ecosystem regressions are visible without hand-running dozens of commands.

The fixture matrix lives in `scripts/pkg-ecosystem-audit.ts` and covers npm, PyPI, Hex, Crates, NuGet, Maven, Zig, vcpkg, Packagist, RubyGems, Go, and Swift. Each registry has three representative packages. `pkg_vulns` failures for vcpkg and Zig are expected and are reported as `expected-unsupported`; all 12 registries are expected to work for `pkg_deps`. Any other failure exits non-zero, including backend data anomalies that should be fixed and rechecked later.
The fixture matrix lives in `scripts/pkg-ecosystem-audit.ts` and covers npm, PyPI, Hex, Crates, NuGet, Maven, Zig, vcpkg, Packagist, RubyGems, Go, Swift, and Pub. Each registry has three representative packages. `pkg_vulns` failures for vcpkg and Zig are expected and are reported as `expected-unsupported`; all 13 registries are expected to work for `pkg_deps`. Any other failure exits non-zero, including backend data anomalies that should be fixed and rechecked later.

Useful invocations:

Expand Down Expand Up @@ -466,7 +486,7 @@ graph), and each transitive occurrence always retains the required
`[]`). `modifiedAt` is included only when it differs from `publishedAt`, and
`isMalicious` is included only when `true`.

**Registry coverage.** npm, PyPI, Hex, Crates, NuGet, Maven, Packagist, RubyGems, Go, and Swift have vulnerability data. vcpkg and Zig are rejected client-side with a tool-specific message (`pkg vulns only supports npm, pypi, hex, crates, nuget, maven, packagist, rubygems, go, and swift. Got: ${registry}.`) — the tool-specific vulnerability predicate lives in `packages/mcp/src/shared/pkgseer-capabilities.ts`.
**Registry coverage.** npm, PyPI, Hex, Crates, NuGet, Maven, Packagist, RubyGems, Go, Swift, and Pub have vulnerability data. vcpkg and Zig are rejected client-side with a tool-specific message (`pkg vulns only supports npm, pypi, hex, crates, nuget, maven, packagist, rubygems, go, swift, and pub. Got: ${registry}.`) — the tool-specific vulnerability predicate lives in `packages/mcp/src/shared/pkgseer-capabilities.ts`.

`pkg_vulns` shares its envelope builder and text formatter with the CLI `githits pkg vulns` command via `packages/mcp/src/shared/package-vulnerabilities-request.ts` and `packages/mcp/src/shared/package-vulnerabilities-response.ts`. MCP defaults to compact text and uses `format: "json"` for structured output. The shared text formatter is surface-aware so MCP hints never mention CLI flags. The parity test (`src/tools/package-vulnerabilities-parity.test.ts`) passes `format: "json"`, asserts `toEqual` across the service-sourced success/filter/typed-error fixtures, and uses `toMatchObject` for builder-sourced `INVALID_ARGUMENT` fixtures such as unsupported registries and tag-style `v`-prefixed versions.

Expand All @@ -476,7 +496,17 @@ graph), and each transitive occurrence always retains the required

- `runtime` block: emitted whenever the service returned `dependencies.direct` (including `{count: 0, items: []}` for zero-dep packages). `runtime.count` is computed client-side from `runtime.items.length`. The source `direct[]` is always runtime-only: dev / peer / build / optional deps live in the groups block instead.
- `groups` block: emitted when the caller requested a lifecycle view and the service returned `dependencyGroups` — including when a lifecycle filter matched nothing (`{items: []}`). Omitted when the service returned `dependencyGroups: null` (e.g. on zero-dep packages), or when the caller used the default runtime view. Each group carries its members under `items`, matching `runtime.items` so dependency lists share one key throughout the envelope. Duplicate `{name, constraint}` entries inside a group are preserved verbatim; the terminal formatter dedups for scannability but JSON is lossless.
- `transitive` block: emitted only when the caller set `max_depth` or requested importer provenance. Carries aggregates (`edges`, `uniquePackages`, `depth?`) plus preprocessed arrays: `packages[]` (each `{name, version, importers[]}` with importer name / version / constraint pulled from the service graph), `conflicts[]` (typed `{name, requiredVersions, requirements}`), and `circularDependencies[]` (typed `{cycle: string[]}`). Each conflict requirement preserves one backend edge in its original multiplicity/order as `{constraint, dependencyType, importer, target}`. Importer and target identities carry registry/name/version; a synthetic root importer is the inspected package with `root: true`. Graph indices never cross this boundary. The raw graph is not exposed — preprocessing happens in the envelope builder so agents consume the same signal the terminal `--verbose` renderer reads without re-implementing the decoder.
- `transitive` block: emitted only when the caller set `max_depth` or requested importer provenance. Carries aggregates (`edges`, `uniquePackages`, `depth?`) plus preprocessed arrays: `packages[]` (each `{registry?, name, version?, importers?}` with importer registry / name / version / constraint pulled from the service graph), `conflicts[]` (typed `{name, requiredVersions, requirements}`), and `circularDependencies[]` (typed `{cycle: string[]}`). Each conflict requirement preserves one backend edge in its original multiplicity/order as `{constraint, dependencyType, importer, target}`. Importer and target identities carry registry/name/version; a synthetic root importer is the inspected package with `root: true`. Graph indices never cross this boundary. The raw graph is not exposed — preprocessing happens in the envelope builder so agents consume the same signal the terminal `--verbose` renderer reads without re-implementing the decoder.

Transitive rows retain the lowercase graph-node `registry` whenever their exact
name/version identifies one node unambiguously, independently of importer output.
Absent graphs, mismatched versions, or ambiguous names across namespaces leave
`registry` and importer provenance omitted rather than inferred. Importer identities
also retain their registry, including SDK namespaces such as `flutter_sdk`; these
namespaces describe graph sources and are not accepted package-target registries.
Text qualifies a package or importer with a parenthesized registry when it differs
from the report header. MCP `include_importers: true` displays provenance in text
as well as JSON; CLI `--verbose` displays the same provenance.

**Dependency issue analysis.** `include_issues: true` requests the backend's lazy
`dependencyIssues` summary and the companion graph needed to resolve conflict
Expand Down Expand Up @@ -532,7 +562,7 @@ JSON retains backend order and multiplicity.

**Typed dependency graph projection.** Backend exposes typed `dependencyGraph`, `dependencyConflicts`, `circularDependencyCycles`, and `environmentMarkers`; `pkg_deps` consumes those typed fields and projects them into a lean agent-facing envelope. Deprecated raw fields (`dag`, `conflicts`, `circularDependencies`, `environmentConstraints`) are intentionally not queried. The raw graph is deliberately not exposed by this tool.

**Registry coverage.** npm, PyPI, Hex, Crates, NuGet, Maven, Zig, vcpkg, Packagist, RubyGems, Go, and Swift support the `packageDependencies` query. The shared registry capability module in `packages/mcp/src/shared/pkgseer-capabilities.ts` supplies the canonical dependency list used by request/help text and the ecosystem audit; it follows `PKGSEER_REGISTRY_ARGS` order. There is no known unsupported dependency registry in this set; only an unknown registry is rejected.
**Registry coverage.** npm, PyPI, Hex, Crates, NuGet, Maven, Zig, vcpkg, Packagist, RubyGems, Go, Swift, and Pub support the `packageDependencies` query. The shared registry capability module in `packages/mcp/src/shared/pkgseer-capabilities.ts` supplies the canonical dependency list used by request/help text and the ecosystem audit; it follows `PKGSEER_REGISTRY_ARGS` order. There is no known unsupported dependency registry in this set; only an unknown registry is rejected.

**Version validation.** Same shared rule as `pkg_vulns`: exact Go inputs are accepted with or without `v` and sent with canonical `v`; unsupported tag-style inputs remain client-side `INVALID_ARGUMENT` errors.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,48 @@ describe("PackageIntelligenceServiceImpl", () => {
expect(result.latestChangelogs?.[0]?.version).toBe("4.18.2");
});

it.each([12345, 0, null])(
"preserves Pub monthly count wire value %s without another request",
async (count) => {
const body = {
data: {
packageSummary: {
package: {
registry: "PUB",
name: "Synthetic_Pub",
latestVersion: "1.2.3",
downloadsLastMonth: count,
},
},
},
};
const fetchFn = mock((_url: unknown, _init?: RequestInit) =>
Promise.resolve(jsonResponse(body)),
);
const service = new PackageIntelligenceServiceImpl(
ENDPOINT,
createMockTokenProvider(),
asFetchFn(fetchFn),
);
const result = await service.packageSummary({
registry: "PUB",
packageName: "Synthetic_Pub",
});
expect(result.package).toMatchObject({
registry: "PUB",
name: "Synthetic_Pub",
latestVersion: "1.2.3",
});
expect(result.package.downloadsLastMonth).toBe(count ?? undefined);
expect(fetchFn).toHaveBeenCalledTimes(1);
const request = JSON.parse(String(fetchFn.mock.calls[0]?.[1]?.body));
expect(request.variables).toMatchObject({
registry: "PUB",
name: "Synthetic_Pub",
});
},
);

it("preserves zero values for summary metadata", async () => {
const body = {
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,8 @@ export interface DirectDependency {
/**
* Node in a typed dependency graph. Mirrors `DependencyGraphNode`.
* `registry` is a lowercase string: `"npm"`, `"pypi"`, …, or
* `"synthetic"` for manifest / project root nodes. `version` is null
* `"flutter_sdk"` for SDK nodes, or `"synthetic"` for project root nodes.
* These graph namespaces are not package registry inputs. `version` is null
* for synthetic roots.
*/
export interface DependencyGraphNode {
Expand Down
9 changes: 9 additions & 0 deletions packages/core-internal/src/shared/pkgseer-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("toPkgseerRegistry", () => {
["rubygems", "RUBYGEMS"],
["go", "GO"],
["swift", "SWIFT"],
["pub", "PUB"],
];

for (const [arg, expected] of cases) {
Expand All @@ -50,6 +51,14 @@ describe("toPkgseerRegistryLowercase", () => {
});

describe("isKnownPkgseerRegistryArg", () => {
it("admits Pub and round-trips its registry values", () => {
expect(isKnownPkgseerRegistryArg("pub")).toBe(true);
expect(knownPkgseerRegistryArgs()).toContain("pub");
expect(PKGSEER_REGISTRY_VALUES).toContain("PUB");
expect(toPkgseerRegistry("pub")).toBe("PUB");
expect(toPkgseerRegistryLowercase("PUB")).toBe("pub");
});

it("accepts every registered lowercase surface value", () => {
for (const arg of knownPkgseerRegistryArgs()) {
expect(isKnownPkgseerRegistryArg(arg)).toBe(true);
Expand Down
2 changes: 2 additions & 0 deletions packages/core-internal/src/shared/pkgseer-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const PKGSEER_REGISTRY_ARGS = [
"rubygems",
"go",
"swift",
"pub",
] as const;

export type PkgseerRegistryArg = (typeof PKGSEER_REGISTRY_ARGS)[number];
Expand All @@ -40,6 +41,7 @@ const registryMap = {
rubygems: "RUBYGEMS",
go: "GO",
swift: "SWIFT",
pub: "PUB",
} as const satisfies Record<PkgseerRegistryArg, string>;

export type PkgseerRegistry = (typeof registryMap)[keyof typeof registryMap];
Expand Down
Loading
Loading