deps

package
v1.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package deps provides dependency resolution from package registries and manifest files.

Overview

Package deps is the core abstraction layer for fetching dependency trees from multiple sources:

  • Package registries: PyPI, npm, crates.io, RubyGems, Packagist, Maven, Go Proxy
  • Manifest files: requirements.txt, package.json, Cargo.toml, poetry.lock, etc.

It provides a concurrent resolver that crawls dependencies in parallel while respecting depth and node limits. The resulting dependency graphs are returned as dag.DAG structures suitable for visualization and analysis.

This package powers the `stacktower parse` command and is language-agnostic, delegating language-specific details to subpackages (python, rust, javascript, etc.).

Architecture

The dependency resolution system has three layers:

  1. Registry integrations ([integrations]): HTTP clients for each registry API
  2. Language definitions (this package): Language values that map registries and manifests
  3. CLI (internal/cli): User commands like `stacktower parse`

Resolving Dependencies

Use a Language's resolver to fetch a complete dependency tree from a registry:

import "github.com/stacktower-io/stacktower/pkg/core/deps/python"

resolver, _ := python.Language.Resolver()
g, _ := resolver.Resolve(ctx, "fastapi", deps.Options{
    MaxDepth: 10,
    MaxNodes: 1000,
})

The resolver crawls dependencies concurrently:

  1. Fetches the root package from the registry
  2. Recursively fetches dependencies up to MaxDepth levels
  3. Builds a dag.DAG with nodes for packages and edges for dependencies
  4. Optionally enriches nodes with metadata from MetadataProvider sources

Options

Options configures resolution behavior. All fields are optional and have sensible defaults via Options.WithDefaults:

  • MaxDepth: Maximum dependency depth (default 50)
  • MaxNodes: Maximum packages to fetch (default 5000)
  • CacheTTL: HTTP cache duration (default 24h)
  • Refresh: Bypass cache to force fresh data
  • MetadataProviders: External enrichment sources (e.g., GitHub, GitLab)
  • Logger: Progress and error callback (must be goroutine-safe)

Package Data

Each resolved package is represented by a Package struct containing:

  • Name, Version: Package identity from the registry
  • Dependencies: Direct dependency names (recursively fetched)
  • Description, License, Author: Registry metadata (availability varies)
  • Repository, HomePage: Source code and documentation URLs
  • Downloads: Popularity metric (when available from the registry)

Use Package.Metadata to convert package fields to a map suitable for dag.Node metadata. Use Package.Ref to create a PackageRef for metadata provider lookups.

Manifest Parsing

For local projects, parse dependency information directly from manifest files:

import "github.com/stacktower-io/stacktower/pkg/core/deps/python"

parsers := python.Language.ManifestParsers(nil)
parser, _ := deps.DetectManifest("poetry.lock", parsers...)
result, _ := parser.Parse("poetry.lock", opts)
g := result.Graph

Manifest parsers implement ManifestParser and vary in completeness:

  • Direct dependencies only: requirements.txt, package.json (base)
  • Full transitive closure: poetry.lock, Cargo.lock, package-lock.json

Use [ManifestParser.IncludesTransitive] to check if additional resolution is needed. Use DetectManifest to find the right parser for a file.

Metadata Enrichment

MetadataProvider implementations add supplementary data from external sources:

import "github.com/stacktower-io/stacktower/pkg/core/deps/metadata"

providers := []deps.MetadataProvider{
    metadata.NewGitHub(cache, token, 24*time.Hour),
}
opts := deps.Options{MetadataProviders: providers}

The GitHub provider (see [metadata.GitHub]) adds:

  • repo_stars: GitHub star count
  • repo_owner, repo_maintainers: Maintainer information
  • repo_last_commit: Last commit timestamp
  • repo_archived: Whether the repository is archived

These fields power Nebraska ranking and brittle package detection.

Supported Languages

Each language has a subpackage exporting a Language definition:

  • [python]: PyPI registry, poetry.lock, requirements.txt, pyproject.toml
  • [rust]: crates.io registry, Cargo.toml, Cargo.lock
  • [javascript]: npm registry, package.json, package-lock.json
  • [ruby]: RubyGems registry, Gemfile, Gemfile.lock
  • [php]: Packagist registry, composer.json, composer.lock
  • [java]: Maven Central registry, pom.xml
  • [golang]: Go Module Proxy, go.mod

Language subpackages also provide registry-specific Fetcher implementations that wrap HTTP clients from the [integrations] package.

Concurrency

The resolver uses a worker pool (20 concurrent goroutines by default) to fetch packages in parallel. All public types are safe for concurrent use:

  • [Fetcher.Fetch] must be goroutine-safe (called by multiple workers)
  • [MetadataProvider.Enrich] must be goroutine-safe (called concurrently per package)
  • [Options.Logger] must be goroutine-safe (called from multiple workers)

Manifest parsers are not required to be goroutine-safe as they are typically called once per file.

Error Handling

Resolution errors fall into two categories:

  • Fatal: Root package not found or unreachable. [Resolver.Resolve] returns an error.
  • Non-fatal: Transitive dependency failures. Logged via [Options.Logger] but don't fail resolution.

Manifest parsing errors are always fatal and returned by [ManifestParser.Parse]. Metadata enrichment errors are non-fatal and logged.

[integrations]: github.com/stacktower-io/stacktower/pkg/integrations internal/cli: github.com/stacktower-io/stacktower/internal/cli dag.DAG: github.com/stacktower-io/stacktower/pkg/core/dag.DAG dag.Node: github.com/stacktower-io/stacktower/pkg/core/dag.Node [python]: github.com/stacktower-io/stacktower/pkg/core/deps/python [rust]: github.com/stacktower-io/stacktower/pkg/core/deps/rust [javascript]: github.com/stacktower-io/stacktower/pkg/core/deps/javascript [ruby]: github.com/stacktower-io/stacktower/pkg/core/deps/ruby [php]: github.com/stacktower-io/stacktower/pkg/core/deps/php [java]: github.com/stacktower-io/stacktower/pkg/core/deps/java [golang]: github.com/stacktower-io/stacktower/pkg/core/deps/golang [metadata.GitHub]: github.com/stacktower-io/stacktower/pkg/core/deps/metadata.GitHub

Example (Constants)
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/deps"
)

func main() {
	// Default values for dependency resolution
	fmt.Println("DefaultMaxDepth:", deps.DefaultMaxDepth)
	fmt.Println("DefaultMaxNodes:", deps.DefaultMaxNodes)
	fmt.Println("DefaultCacheTTL:", deps.DefaultCacheTTL)
}
Output:
DefaultMaxDepth: 50
DefaultMaxNodes: 5000
DefaultCacheTTL: 24h0m0s

Index

Examples

Constants

View Source
const (
	// DefaultMaxDepth is the default maximum dependency depth (50 levels).
	// This prevents infinite recursion in circular or very deep dependency trees.
	// Note: pipeline.DefaultMaxDepth (10) is more conservative for CLI/API UX,
	// but this higher limit is appropriate for the low-level resolver.
	DefaultMaxDepth = 50

	// DefaultMaxNodes is the default maximum number of packages to fetch (5000 nodes).
	// This caps memory usage and prevents unbounded crawling of large ecosystems.
	// This value is shared with pipeline.DefaultMaxNodes.
	DefaultMaxNodes = 5000

	// DefaultCacheTTL is the default HTTP cache duration (24 hours).
	// Cached registry responses are reused within this window unless Refresh is true.
	DefaultCacheTTL = 24 * time.Hour

	// DependencyScopeProdOnly keeps only production/runtime dependencies.
	DependencyScopeProdOnly = "prod_only"

	// DependencyScopeAll keeps all dependencies including dev/test.
	DependencyScopeAll = "all"
)
View Source
const DefaultWorkers = 20

DefaultWorkers is the default number of concurrent goroutines for fetching packages. This limits parallelism to prevent overwhelming registries and to bound memory usage. Set to 20 to match the burst limit of most registry rate limiters.

View Source
const ProjectRootNodeID = dag.ProjectRootNodeID

ProjectRootNodeID is an alias for dag.ProjectRootNodeID so that callers within the deps package (and its sub-packages) can use the short name.

Variables

This section is empty.

Functions

func BuildPackageID

func BuildPackageID(name, version, commit string) string

BuildPackageID constructs a versioned package identifier.

The format depends on the inputs:

  • With commit: "name@commit:sha"
  • With version: "name@version"
  • Neither: "name"

Commit takes precedence over version if both are provided. Empty name returns an empty string.

func FilterPrereleaseNodes

func FilterPrereleaseNodes(g *dag.DAG, includePrerelease bool) *dag.DAG

FilterPrereleaseNodes removes prerelease versions from the graph unless includePrerelease is true.

func GetManifestLanguage

func GetManifestLanguage(filename string, languages []*Language) string

GetManifestLanguage returns the language name for a manifest file, if supported. Returns empty string if the manifest is not supported.

func IsManifestSupported

func IsManifestSupported(filename string, languages []*Language) bool

IsManifestSupported checks if a manifest filename is supported by any of the languages.

func IsPrereleaseVersion

func IsPrereleaseVersion(version string) bool

IsPrereleaseVersion checks if a version string represents a prerelease. It detects common prerelease markers like alpha, beta, rc, dev, canary, nightly, next, etc., as well as PEP 440 style markers (e.g., 1.0.0a1).

func NormalizeLanguageName

func NormalizeLanguageName(name string, languages []*Language) string

NormalizeLanguageName maps external language names (e.g., from GitHub API) to our standard internal names. Returns the original (lowercased) if no mapping exists.

func ParallelMapOrdered

func ParallelMapOrdered[I any, O any](
	ctx context.Context,
	workers int,
	items []I,
	fn func(context.Context, I) O,
) ([]O, error)

ParallelMapOrdered applies fn to items with bounded concurrency and returns results in the same order as the input slice.

Context cancellation is respected: if the context is cancelled before or during execution, the function returns nil and ctx.Err(). Workers check context before processing each item and stop early on cancellation.

func ParsePackageID

func ParsePackageID(id string) (name, version, commit string)

ParsePackageID parses a versioned package identifier into its components.

Scoped npm names (starting with "@") are handled correctly by splitting on the last "@" separator rather than the first.

Examples:

  • "requests@2.31.0" → ("requests", "2.31.0", "")
  • "lib@commit:abc1234" → ("lib", "", "abc1234")
  • "requests" → ("requests", "", "")
  • "@types/node@20.1.0" → ("@types/node", "20.1.0", "")
  • "@types/node" → ("@types/node", "", "")

Returns the package name, version, and commit. At most one of version or commit will be non-empty.

func ResolveAndMerge

func ResolveAndMerge(ctx context.Context, resolver Resolver, dependencies []Dependency, opts Options) (*dag.DAG, error)

ResolveAndMerge resolves each dependency via the resolver and merges the results into a single DAG with a virtual project root. Dependencies that fail to resolve are added as leaf nodes with a direct edge from the root.

This is the shared implementation used by all manifest parsers that support transitive resolution (e.g., requirements.txt + resolver, Cargo.toml + resolver).

When a dependency has a Pinned version, the resolver will fetch that exact version rather than the latest. This is important for lock files and go.mod where versions are explicitly specified.

func ShallowGraphFromDeps

func ShallowGraphFromDeps(dependencies []Dependency) *dag.DAG

ShallowGraphFromDeps creates a shallow dependency graph with only direct dependencies. The graph has a virtual project root (ProjectRootNodeID) connected to each dependency. This is the standard pattern for manifest parsers that don't resolve transitive dependencies.

func SupportedManifests

func SupportedManifests(languages []*Language) map[string]string

SupportedManifests returns a map of filename -> language for all supported manifests. This is a convenience function for quick lookups.

Types

type BatchMetadataProvider

type BatchMetadataProvider interface {
	MetadataProvider

	// EnrichBatch fetches metadata for multiple packages in one operation.
	// Returns a map keyed by package name to its metadata map.
	// Packages that fail enrichment are silently omitted from the result.
	EnrichBatch(ctx context.Context, pkgs []*PackageRef, refresh bool) (map[string]map[string]any, error)
}

BatchMetadataProvider extends MetadataProvider with bulk enrichment. Instead of N individual Enrich calls (each hitting rate limits), a single EnrichBatch call fetches metadata for all packages at once -- typically via a batched API like GitHub GraphQL.

The resolver checks for this interface and uses it when available, falling back to per-package Enrich calls otherwise.

type CargoDetector

type CargoDetector struct{}

CargoDetector detects dependency sections in Cargo.toml files.

func (*CargoDetector) DetectSections

func (d *CargoDetector) DetectSections(content string) []SourceRange

func (*CargoDetector) Supports

func (d *CargoDetector) Supports(filename string) bool

type CargoLockDetector

type CargoLockDetector struct{}

CargoLockDetector detects package sections in Cargo.lock files.

func (*CargoLockDetector) DetectSections

func (d *CargoLockDetector) DetectSections(content string) []SourceRange

func (*CargoLockDetector) Supports

func (d *CargoLockDetector) Supports(filename string) bool

type ComposerDetector

type ComposerDetector struct{}

ComposerDetector detects dependency sections in composer.json files.

func (*ComposerDetector) DetectSections

func (d *ComposerDetector) DetectSections(content string) []SourceRange

func (*ComposerDetector) Supports

func (d *ComposerDetector) Supports(filename string) bool

type ConstraintParser

type ConstraintParser interface {
	// ParseConstraint converts an ecosystem-specific constraint string to a
	// PubGrub Condition. Returns nil if the constraint is empty or invalid.
	ParseConstraint(constraint string) pubgrub.Condition

	// ParseVersion converts a version string to a PubGrub Version.
	ParseVersion(version string) pubgrub.Version
}

ConstraintParser converts ecosystem-specific version constraints to PubGrub format. Different ecosystems use different constraint syntaxes (PEP 440, semver, etc.), so each provides its own parser implementation.

type CratesURLProvider

type CratesURLProvider struct {
	// contains filtered or unexported fields
}

CratesURLProvider fetches package URLs from crates.io in parallel. The underlying crates.Client handles rate limiting (5 req/s, burst 10).

func NewCratesURLProvider

func NewCratesURLProvider(c cache.Cache, cacheTTL time.Duration) *CratesURLProvider

NewCratesURLProvider creates a new crates.io URL provider.

func (*CratesURLProvider) FetchURLs

func (p *CratesURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given crate names from crates.io. Packages are processed in chunks of 50 for better progress feedback.

type Dependency

type Dependency struct {
	// Name is the normalized package name (e.g., "requests", "lodash").
	Name string

	// Constraint is the version constraint from the manifest file (e.g., "^4.17.0",
	// ">=2.0", "~1.0"). Empty if no constraint was specified (meaning "latest").
	Constraint string

	// Pinned is the resolved/pinned version (e.g., "4.17.21"). This is set when
	// the exact version is known, either from a lock file or after resolution.
	// Empty if the version hasn't been resolved yet.
	Pinned string

	// Commit is the git commit SHA for private repos or packages without semantic
	// versions. Takes precedence over Pinned when building the versioned ID.
	// Format: full SHA or abbreviated (e.g., "abc1234").
	Commit string
}

Dependency represents a package dependency with version information.

This struct captures both the declared constraint (from manifest files) and the resolved version (from lock files or registry lookups). For private repos without semantic versions, the Commit field tracks the git commit SHA.

func DependencyFromName

func DependencyFromName(name string) Dependency

DependencyFromName creates a Dependency with only the name set. This is a convenience function for backward compatibility when only the package name is known.

func DependencyFromNameConstraint

func DependencyFromNameConstraint(name, constraint string) Dependency

DependencyFromNameConstraint creates a Dependency with name and constraint.

func DependencyFromNameVersion

func DependencyFromNameVersion(name, version string) Dependency

DependencyFromNameVersion creates a Dependency with name and pinned version.

func (Dependency) ID

func (d Dependency) ID() string

ID returns the unique identifier for this dependency.

The format depends on what version information is available:

  • With commit: "name@commit:sha" (e.g., "my-lib@commit:abc1234")
  • With pinned version: "name@version" (e.g., "requests@2.31.0")
  • Unresolved: "name" (e.g., "requests")

This ID is suitable for use as a dag.Node.ID and uniquely identifies the specific version of the package in the dependency graph.

func (Dependency) IsResolved

func (d Dependency) IsResolved() bool

IsResolved reports whether this dependency has a resolved version or commit.

func (Dependency) NameOnly

func (d Dependency) NameOnly() string

NameOnly returns just the package name without version information. This is useful for backward compatibility with name-only lookups.

func (Dependency) VersionConstraint

func (d Dependency) VersionConstraint() string

VersionConstraint returns the version constraint string for edge metadata. Returns Constraint if set, otherwise Pinned.

type DiamondDependencyError

type DiamondDependencyError struct {
	Package     string   // The package with conflicting requirements
	Dependents  []string // Packages that depend on conflicting versions
	Language    string   // Ecosystem (e.g., "javascript")
	OriginalErr string   // Original resolver error message
}

DiamondDependencyError is returned when dependency resolution fails due to conflicting version requirements for the same package from different dependents. This is common in npm where packages allow nested/duplicate dependencies, but our SAT solver requires a single version per package.

func (*DiamondDependencyError) Error

func (e *DiamondDependencyError) Error() string

type EnrichStats

type EnrichStats struct {
	// Total is the number of packages that were candidates for enrichment.
	Total int
	// Succeeded is the number of packages that were successfully enriched
	// (metadata was added to the node).
	Succeeded int
	// Failed is the number of packages where enrichment failed.
	Failed int
	// AuthError indicates if an authentication error was encountered.
	// This typically means a GitHub token is missing, expired, or invalid.
	AuthError bool
	// UsedBatch indicates if batch enrichment was used (vs per-package fallback).
	UsedBatch bool
}

EnrichStats contains statistics about the enrichment process. This is useful for observability and debugging.

func EnrichGraph

func EnrichGraph(ctx context.Context, g *dag.DAG, manifestFile string, opts Options) EnrichStats

EnrichGraph adds external metadata (e.g. GitHub stars) to every non-root graph node. It prefers batch providers (one API call for all packages) and falls back to parallel per-package enrichment using ParallelMapOrdered.

The manifestFile parameter specifies the manifest file name for PackageRef (e.g., "package.json", "Cargo.toml", "pyproject.toml").

If opts.URLProvider is set, it fetches package URLs from the registry before enrichment. This enables GitHub enrichment for manifest files that don't include repository URLs directly (e.g., lock files).

Returns EnrichStats with counts of successful/failed enrichments for observability. This is the standard enrichment pattern for all lock file parsers.

type Fetcher

type Fetcher interface {
	// Fetch retrieves package information by name (latest version).
	//
	// The name is the package identifier in the registry (e.g., "requests", "serde").
	// If refresh is true, cached HTTP responses are bypassed and fresh data is fetched.
	//
	// Returns an error if:
	//   - The package does not exist in the registry
	//   - The registry API is unreachable or returns an error
	//   - The response cannot be parsed
	//
	// Implementations should respect context cancellation and return ctx.Err()
	// when the context is canceled.
	//
	// Fetch must be safe for concurrent use by multiple goroutines.
	Fetch(ctx context.Context, name string, refresh bool) (*Package, error)

	// FetchVersion retrieves package information for a specific version.
	//
	// The name is the package identifier, and version is the exact version to fetch.
	// If refresh is true, cached HTTP responses are bypassed.
	//
	// Returns an error if:
	//   - The package or version does not exist in the registry
	//   - The registry API is unreachable or returns an error
	//   - The response cannot be parsed
	//
	// FetchVersion must be safe for concurrent use by multiple goroutines.
	FetchVersion(ctx context.Context, name, version string, refresh bool) (*Package, error)
}

Fetcher retrieves package metadata from a registry.

Implementations wrap HTTP clients for specific registries (PyPI, npm, crates.io). The Fetcher is responsible for HTTP caching, rate limiting, and error handling.

Fetchers are found in the integrations subpackages (e.g., integrations/pypi).

type GemfileDetector

type GemfileDetector struct{}

GemfileDetector detects dependency sections in Gemfile files.

func (*GemfileDetector) DetectSections

func (d *GemfileDetector) DetectSections(content string) []SourceRange

func (*GemfileDetector) Supports

func (d *GemfileDetector) Supports(filename string) bool

type GoModDetector

type GoModDetector struct{}

GoModDetector detects dependency sections in go.mod files.

func (*GoModDetector) DetectSections

func (d *GoModDetector) DetectSections(content string) []SourceRange

func (*GoModDetector) Supports

func (d *GoModDetector) Supports(filename string) bool

type GoProxyURLProvider

type GoProxyURLProvider struct {
	// contains filtered or unexported fields
}

GoProxyURLProvider fetches module URLs from Go module proxy in parallel. The underlying goproxy.Client handles rate limiting.

func NewGoProxyURLProvider

func NewGoProxyURLProvider(c cache.Cache, cacheTTL time.Duration) *GoProxyURLProvider

NewGoProxyURLProvider creates a new Go module proxy URL provider.

func (*GoProxyURLProvider) FetchURLs

func (p *GoProxyURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given module paths from Go proxy. Packages are processed in chunks of 50 for better progress feedback.

type IncompatibleRuntimeError

type IncompatibleRuntimeError struct {
	Package           string // Package name
	Version           string // Package version
	RuntimeConstraint string // Package's runtime requirement (e.g., ">=3.8")
	TargetRuntime     string // Target runtime version (e.g., "2.7")
}

IncompatibleRuntimeError is returned when a package requires a runtime version that is incompatible with the target runtime version.

func (*IncompatibleRuntimeError) Error

func (e *IncompatibleRuntimeError) Error() string

type Language

type Language struct {
	// Name is the language identifier (e.g., "python", "rust", "javascript").
	Name string

	// DefaultRegistry is the primary registry name for this language
	// (e.g., "pypi" for Python, "crates" for Rust). Used when no registry
	// is explicitly specified.
	DefaultRegistry string

	// DefaultRuntimeVersion is the default runtime version to use when none
	// is specified via CLI or manifest. For example, "3.11" for Python,
	// "22.12.0" for Node.js, "1.75" for Rust. Empty string means no default.
	DefaultRuntimeVersion string

	// RegistryAliases maps alternative registry names to canonical names.
	// For example, {"npm": "npm", "npmjs": "npm"}. May be nil or empty.
	RegistryAliases map[string]string

	// ManifestTypes lists supported manifest type identifiers
	// (e.g., ["poetry", "requirements", "pipfile"] for Python). These are
	// the canonical names passed to NewManifest. May be nil or empty.
	ManifestTypes []string

	// ManifestAliases maps filenames to manifest types. For example,
	// {"poetry.lock": "poetry", "requirements.txt": "requirements"}.
	// Used by DetectManifest to match file paths. May be nil or empty.
	ManifestAliases map[string]string

	// NewResolver creates a registry Resolver with the given cache and options.
	// The cache is used for HTTP response caching (use cache.NullCache{} for no caching).
	// Options provides language-specific configuration (e.g., RuntimeVersion for Python).
	// Returns an error if resolver construction fails (e.g., missing
	// configuration). May be nil if the language has no registry support.
	NewResolver func(c cache.Cache, opts Options) (Resolver, error)

	// NewManifest creates a ManifestParser for the given type name and resolver.
	// The name is typically a value from ManifestTypes or ManifestAliases.
	// Returns nil if the type is unrecognized. May be nil if the language
	// has no manifest support. The resolver may be nil for parsers that don't
	// need to fetch additional data.
	NewManifest func(name string, res Resolver) ManifestParser

	// ManifestParsers returns all available ManifestParser implementations
	// for this language. The resolver is passed to each parser and may be nil.
	// Returns nil or an empty slice if the language has no manifest support.
	ManifestParsers func(res Resolver) []ManifestParser

	// NormalizeName transforms a package name to its canonical form.
	// For example, Maven coordinates may use underscores as a filesystem-safe
	// alternative to colons: "com.google.guava_guava" -> "com.google.guava:guava".
	// May be nil if the language doesn't require name normalization.
	NormalizeName func(name string) string
}

Language defines how to resolve dependencies for a programming language.

Each language subpackage (python, rust, javascript, etc.) exports a Language value that describes its registry API, manifest formats, and how to construct resolvers and parsers.

Language values are typically used by the CLI to dispatch commands like "stacktower parse" based on file type or registry name.

func FindLanguage

func FindLanguage(name string, languages []*Language) *Language

FindLanguage returns the Language with the given name from the provided list, or nil if not found.

func (*Language) HasManifests

func (l *Language) HasManifests() bool

HasManifests reports whether this language supports manifest file parsing.

Returns true if NewManifest is non-nil, meaning at least one manifest format is supported. This does not guarantee that a specific manifest type is recognized—use Manifest to check.

func (*Language) Manifest

func (l *Language) Manifest(name string, res Resolver) (ManifestParser, bool)

Manifest returns a parser for the named manifest type, resolving aliases.

The name is first resolved through ManifestAliases (e.g., "poetry.lock" -> "poetry"), then passed to NewManifest. The resolver may be nil for parsers that don't fetch additional data.

Returns (parser, true) if successful, or (nil, false) if:

  • NewManifest is nil (language has no manifest support)
  • The manifest type is unrecognized by NewManifest

Safe to call on a zero Language value.

func (*Language) Registry

func (l *Language) Registry(backend cache.Cache, name string, opts Options) (Resolver, error)

Registry returns a Resolver for the named registry, resolving aliases.

The name is first resolved through RegistryAliases. If it doesn't match DefaultRegistry, an error is returned. This method currently only supports a single registry per language.

The resolver is created with the given backend and options. Use DefaultCacheTTL in opts.CacheTTL if not specified.

Returns an error if the registry name is unknown or if NewResolver fails.

func (*Language) Resolver

func (l *Language) Resolver(backend cache.Cache, opts Options) (Resolver, error)

Resolver returns the default registry resolver for this language.

This is a convenience wrapper around NewResolver with the given backend and default options. Returns an error if NewResolver is nil or fails.

type ManifestInfo

type ManifestInfo struct {
	// Filename is the manifest file name (e.g., "package.json", "go.mod").
	Filename string

	// Language is the language name (e.g., "python", "javascript").
	Language string

	// ManifestType is the internal type identifier (e.g., "poetry", "cargo").
	ManifestType string

	// Supported indicates whether stacktower can parse this manifest.
	Supported bool
}

ManifestInfo describes a manifest file and its support status.

func KnownManifests

func KnownManifests(languages []*Language, extraUnsupported map[string]string) []ManifestInfo

KnownManifests lists all manifest files that stacktower knows about. This includes both supported manifests (from Language.ManifestAliases) and commonly encountered manifests that are not yet supported.

The languages parameter should contain all Language definitions to aggregate. Additional unsupported manifests can be added via extraUnsupported.

type ManifestParser

type ManifestParser interface {
	// Parse reads the manifest file at path and returns the dependency graph.
	//
	// The path is typically a local file system path. Options may influence
	// parsing behavior (e.g., MaxDepth for resolvers that fetch additional data).
	//
	// Returns an error if the file cannot be read, is malformed, or if
	// dependency resolution fails. Common errors:
	//   - File not found or unreadable
	//   - Invalid JSON/TOML/YAML syntax
	//   - Missing required fields
	//   - Dependency fetching failures (if the parser resolves transitive deps)
	Parse(path string, opts Options) (*ManifestResult, error)

	// Supports reports whether this parser handles the given filename.
	//
	// The filename is typically the basename of a path (e.g., "package.json").
	// Returns true if this parser recognizes the file format.
	Supports(filename string) bool

	// Type returns the manifest type identifier (e.g., "poetry", "cargo", "npm").
	//
	// This identifier appears in ManifestResult.Type and is used for
	// logging and error messages.
	Type() string

	// IncludesTransitive reports whether this parser produces transitive deps.
	//
	// Returns true for lock files (poetry.lock, Cargo.lock) that contain the
	// full dependency closure. Returns false for requirement files (requirements.txt,
	// package.json) that only list direct dependencies.
	//
	// This is used by the CLI to decide whether additional resolution is needed.
	IncludesTransitive() bool
}

ManifestParser reads dependency information from local manifest files.

Manifest files describe a project's dependencies and may be either:

  • Requirement files (package.json, requirements.txt) with direct deps only
  • Lock files (poetry.lock, Cargo.lock) with full transitive closures

Implementations are found in language subpackages (e.g., python.PoetryParser).

func DetectManifest

func DetectManifest(path string, parsers ...ManifestParser) (ManifestParser, error)

DetectManifest finds a parser that supports the given file path.

The path is matched against each parser's Supports method using the basename. Parsers are checked in order, and the first match is returned.

Typical usage:

lang := python.Language
parsers := lang.ManifestParsers(nil)
parser, err := deps.DetectManifest("poetry.lock", parsers...)
if err != nil {
    // No parser supports poetry.lock
}
result, err := parser.Parse("poetry.lock", opts)

Returns an error if no parser in the list supports the file. An empty parsers list always returns an error.

Example
package main

import (
	"fmt"
)

func main() {
	// DetectManifest finds the right parser for a manifest file.
	// In real usage, you would get parsers from a Language definition:
	//
	//   import "github.com/stacktower-io/stacktower/pkg/core/deps/python"
	//
	//   parsers := python.Language.ManifestParsers(nil)
	//   parser, err := deps.DetectManifest("poetry.lock", parsers...)
	//   if err != nil {
	//       log.Fatal(err)
	//   }
	//   result, err := parser.Parse("poetry.lock", opts)
	//
	// The function matches filename patterns to find a suitable parser.
	// Returns an error if no parser recognizes the file.

	fmt.Println("DetectManifest matches filename to parser type")
}
Output:
DetectManifest matches filename to parser type

type ManifestResult

type ManifestResult struct {
	// Graph is the dependency graph with nodes for packages and edges
	// for dependencies.
	Graph *dag.DAG

	// Type is the manifest type identifier (from ManifestParser.Type).
	// Examples: "poetry", "cargo", "npm", "requirements".
	Type string

	// IncludesTransitive indicates whether Graph contains the full transitive
	// closure (true for lock files) or just direct dependencies (false).
	IncludesTransitive bool

	// RootPackage is the name of the root package, if determinable from the
	// manifest. Empty if the manifest doesn't specify a package name (e.g.,
	// requirements.txt has no root package).
	RootPackage string

	// RuntimeVersion is the target runtime version detected from the manifest.
	// For Python: extracted from requires-python (e.g., ">=3.9" → "3.9")
	// For Node.js: extracted from engines.node
	// For Ruby: extracted from ruby version directive
	// Empty if not specified; callers should use a language-specific default.
	RuntimeVersion string

	// RuntimeConstraint is the raw runtime constraint from the manifest.
	// For Python: ">=3.9,<4.0"
	// For Node.js: ">=18"
	// Empty if not specified.
	RuntimeConstraint string
}

ManifestResult holds the parsed dependency data from a manifest file.

Returned by [ManifestParser.Parse] after successfully reading a manifest.

type MavenURLProvider

type MavenURLProvider struct {
	// contains filtered or unexported fields
}

MavenURLProvider fetches artifact URLs from Maven Central in parallel. The underlying maven.Client handles rate limiting.

func NewMavenURLProvider

func NewMavenURLProvider(c cache.Cache, cacheTTL time.Duration) *MavenURLProvider

NewMavenURLProvider creates a new Maven URL provider.

func (*MavenURLProvider) FetchURLs

func (p *MavenURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given artifact coordinates from Maven. Coordinates should be in the format "groupId:artifactId". Packages are processed in chunks of 50 for better progress feedback.

type MetadataProvider

type MetadataProvider interface {
	// Name returns the provider identifier (e.g., "github", "gitlab").
	// This is used for logging and error messages.
	Name() string

	// Enrich fetches additional metadata for the package.
	//
	// The pkg parameter contains registry information and URLs for lookup.
	// If refresh is true, the provider should bypass its cache.
	//
	// Returns a map of metadata keys to values, which are merged into the
	// package node's metadata. Keys should be provider-specific (e.g.,
	// "github_stars") to avoid conflicts with other providers.
	//
	// Returns an error if enrichment fails. The resolver logs the error
	// but continues without failing the entire resolution.
	Enrich(ctx context.Context, pkg *PackageRef, refresh bool) (map[string]any, error)
}

MetadataProvider enriches package nodes with external data (e.g., GitHub stars).

Implementations fetch supplementary information that is not available in package registries, such as repository activity, maintainer counts, or security metrics. Providers are called concurrently after fetching each package during resolution.

type NpmURLProvider

type NpmURLProvider struct {
	// contains filtered or unexported fields
}

NpmURLProvider fetches package URLs from npm in parallel. The underlying npm.Client handles rate limiting (50 req/s, burst 30).

func NewNpmURLProvider

func NewNpmURLProvider(c cache.Cache, cacheTTL time.Duration) *NpmURLProvider

NewNpmURLProvider creates a new npm URL provider.

func (*NpmURLProvider) FetchURLs

func (p *NpmURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given package names from npm. Packages are processed in chunks of 50 for better progress feedback.

type Options

type Options struct {
	// Ctx is the context for cancellation and timeouts. If nil, WithDefaults
	// replaces it with context.Background(). Manifest parsers and resolvers
	// use this context for network operations and enrichment so that callers
	// can cancel long-running parses.
	Ctx context.Context

	// Version constrains the root package to a specific version. If empty,
	// the latest version is fetched. This only applies to the root package;
	// transitive dependencies are resolved normally.
	// Example: "2.31.0" for requests@2.31.0
	Version string

	// Constraint constrains the root package to a version range (ecosystem
	// syntax), such as "^4.18.0" (npm) or ">=1.0, <2.0". If both Version and
	// Constraint are set, Version takes precedence.
	Constraint string

	// DependencyScope controls which dependency groups parsers include.
	// Supported values:
	//   - "prod_only" (default): runtime/production dependencies only
	//   - "all": include dev/test dependencies too
	DependencyScope string

	// MaxDepth limits how many levels deep to traverse. A value of 1 fetches
	// only direct dependencies. Zero or negative values use DefaultMaxDepth (50).
	MaxDepth int

	// MaxNodes limits the total number of packages to fetch. When this limit
	// is reached, deeper dependencies are ignored but already-queued packages
	// may still be fetched. Zero or negative values use DefaultMaxNodes (5000).
	MaxNodes int

	// Workers is the number of concurrent goroutines for fetching packages.
	// Higher values increase parallelism but may trigger rate limits.
	// Zero or negative values use DefaultWorkers (20).
	Workers int

	// CacheTTL controls how long HTTP responses are cached. Registry clients
	// will reuse cached data within this duration. Zero or negative values use
	// DefaultCacheTTL (24 hours).
	CacheTTL time.Duration

	// Refresh bypasses the HTTP cache when true, forcing fresh registry fetches.
	// This is useful for getting the latest package versions but increases latency.
	Refresh bool

	// MetadataProviders is an optional list of enrichment sources (e.g., GitHub)
	// that add extra metadata to package nodes. Providers are called concurrently
	// after fetching each package. Nil or empty is safe.
	MetadataProviders []MetadataProvider

	// Logger is an optional callback for progress and error messages. If nil,
	// WithDefaults replaces it with a no-op logger. The format string follows
	// fmt.Printf conventions. Logger may be called concurrently from multiple
	// goroutines and must be safe for concurrent use.
	Logger func(string, ...any)

	// IncludePrerelease controls whether prerelease versions (alpha, beta, rc,
	// dev, etc.) are considered during resolution. When false (the default), the
	// resolver filters out prerelease versions before version selection.
	IncludePrerelease bool

	// RuntimeVersion is the target runtime version for environment marker evaluation.
	// For Python, this filters deps with markers like `python_version < "3.11"`.
	// For Node.js, this would filter based on `engines.node` constraints.
	// Empty means use the language-specific default (e.g., "3.11" for Python).
	RuntimeVersion string

	// URLProvider fetches repository URLs from the package registry.
	// Used by EnrichGraph to populate PackageRef.ProjectURLs for manifest parsing.
	// If nil, packages without URLs in node metadata won't be enriched with
	// external data like GitHub stars.
	URLProvider URLProvider
}

Options configures dependency resolution behavior.

All fields are optional. Zero values are replaced by defaults when passed to WithDefaults. Options is safe to copy and does not modify any inputs.

Example (Limits)
package main

import (
	"fmt"
	"time"

	"github.com/stacktower-io/stacktower/pkg/core/deps"
)

func main() {
	// Configure resolution limits for large dependency trees
	opts := deps.Options{
		MaxDepth: 10,  // Stop at 10 levels deep
		MaxNodes: 100, // Fetch at most 100 packages
		CacheTTL: time.Hour,
		Refresh:  true, // Bypass cache for fresh data
	}

	fmt.Println("MaxDepth:", opts.MaxDepth)
	fmt.Println("MaxNodes:", opts.MaxNodes)
	fmt.Println("Refresh:", opts.Refresh)
}
Output:
MaxDepth: 10
MaxNodes: 100
Refresh: true
Example (WithLogger)
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/deps"
)

func main() {
	// Custom logger for progress tracking
	var logs []string
	opts := deps.Options{
		MaxDepth: 5,
		Logger: func(format string, args ...any) {
			logs = append(logs, fmt.Sprintf(format, args...))
		},
	}.WithDefaults()

	// Logger is preserved through WithDefaults
	opts.Logger("Fetching %s", "fastapi")
	opts.Logger("Found %d dependencies", 12)

	fmt.Println("Log count:", len(logs))
}
Output:
Log count: 2

func (Options) WithDefaults

func (o Options) WithDefaults() Options

WithDefaults returns a copy of Options with zero values replaced by defaults.

This method is safe to call on a zero Options value. It fills in:

  • MaxDepth: DefaultMaxDepth (50)
  • MaxNodes: DefaultMaxNodes (5000)
  • CacheTTL: DefaultCacheTTL (24h)
  • Logger: no-op function if nil

All other fields (Refresh, MetadataProviders) are preserved as-is, including nil slices. The original Options value is not modified.

Example
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/deps"
)

func main() {
	// Create options with some custom values
	opts := deps.Options{
		MaxDepth: 10,
		// MaxNodes and CacheTTL left as zero - will get defaults
	}

	// Apply defaults to fill in missing values
	opts = opts.WithDefaults()

	fmt.Println("MaxDepth:", opts.MaxDepth)
	fmt.Println("MaxNodes:", opts.MaxNodes)
	fmt.Println("CacheTTL:", opts.CacheTTL)
}
Output:
MaxDepth: 10
MaxNodes: 5000
CacheTTL: 24h0m0s

type Package

type Package struct {
	// Name is the package identifier in the registry (e.g., "requests", "serde").
	Name string

	// Version is the package version (e.g., "2.31.0"). For registry lookups
	// without a version constraint, this is typically the latest stable version.
	Version string

	// Commit is the git commit SHA for private repos or packages without semantic
	// versions. When set, this takes precedence over Version for building the
	// package's versioned ID.
	Commit string

	// Dependencies lists direct dependencies with their version constraints.
	// The resolver recursively fetches these to build the dependency tree.
	// Nil and empty slices are equivalent.
	Dependencies []Dependency

	// Description is a short summary of the package purpose. May be empty.
	Description string

	// License is the package license identifier (e.g., "MIT", "Apache-2.0").
	// May be empty or unknown.
	License string

	// LicenseText is the full raw license text for custom/proprietary licenses.
	// This is populated when the license field contains a long custom license
	// (e.g., custom commercial terms) rather than a standard SPDX identifier.
	// Intended for downstream LLM analysis of non-standard licenses.
	// May be empty for standard licenses where the text is well-known.
	LicenseText string

	// Author is the primary package author or maintainer. May be empty.
	Author string

	// Downloads is the total download count or recent download rate, depending
	// on the registry. Zero if unavailable. Not all registries provide this.
	Downloads int

	// Repository is the source code repository URL (e.g., GitHub, GitLab).
	// May be empty if not specified in registry metadata.
	Repository string

	// HomePage is the project home page URL. May be empty or identical to Repository.
	HomePage string

	// ProjectURLs contains additional URLs from the registry (docs, issues, etc.).
	// Keys and availability vary by registry. May be nil.
	ProjectURLs map[string]string

	// ManifestFile identifies the manifest type when this Package comes from
	// manifest parsing (e.g., "poetry", "cargo"). Empty for registry packages.
	ManifestFile string

	// RuntimeConstraint is the runtime version constraint (e.g., ">=3.8" for Python).
	// Used to check compatibility with target runtime versions. May be empty.
	RuntimeConstraint string
}

Package holds metadata fetched from a package registry.

This is the core data structure returned by [Fetcher.Fetch] and used throughout the resolution process. Not all fields are populated by every registry—consult the specific integration documentation for field availability.

func (*Package) DependencyNames

func (p *Package) DependencyNames() []string

DependencyNames returns the dependency names as a slice of strings. This is a backward compatibility helper for code that only needs names.

func (*Package) ID

func (p *Package) ID() string

ID returns the versioned identifier for this package.

The format depends on what version information is available:

  • With commit: "name@commit:sha"
  • With version: "name@version"
  • Neither: "name"

func (*Package) Metadata

func (p *Package) Metadata() map[string]any

Metadata converts Package fields to a map for node metadata.

The returned map always contains "version". Optional fields (description, license, author, downloads, commit) are included only if non-empty/non-zero.

This map is suitable for use as dag.Node.Meta and can be further enriched by MetadataProvider implementations. The map is newly allocated and safe to modify. Returns a non-nil map even if the Package has no optional fields.

Example
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/deps"
)

func main() {
	// Package metadata from a registry
	pkg := deps.Package{
		Name:        "fastapi",
		Version:     "0.100.0",
		Description: "FastAPI framework, high performance",
		License:     "MIT",
		Author:      "Sebastián Ramírez",
		Downloads:   1000000,
	}

	// Convert to node metadata map
	meta := pkg.Metadata()

	fmt.Println("version:", meta["version"])
	fmt.Println("license:", meta["license"])
	fmt.Println("has description:", meta["description"] != nil)
}
Output:
version: 0.100.0
license: MIT
has description: true

func (*Package) Ref

func (p *Package) Ref() *PackageRef

Ref creates a PackageRef for metadata provider lookups.

The returned PackageRef consolidates URL information from multiple Package fields (ProjectURLs, Repository, HomePage) into a single ProjectURLs map for convenient provider lookups.

The ProjectURLs map is a clone of the original, so modifying it does not affect the Package. If the Package has nil ProjectURLs, an empty map is allocated. Repository and HomePage are added to the map under "repository" and "homepage" keys if non-empty.

This method never returns nil. Safe to call on a zero Package value.

Example
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/deps"
)

func main() {
	// Package with repository information
	pkg := deps.Package{
		Name:       "requests",
		Version:    "2.31.0",
		Repository: "https://github.com/psf/requests",
		HomePage:   "https://requests.readthedocs.io",
	}

	// Create a reference for metadata enrichment lookups
	ref := pkg.Ref()

	fmt.Println("Name:", ref.Name)
	fmt.Println("Repository URL:", ref.ProjectURLs["repository"])
}
Output:
Name: requests
Repository URL: https://github.com/psf/requests

func (*Package) SetDependenciesFromNames

func (p *Package) SetDependenciesFromNames(names []string)

SetDependenciesFromNames sets dependencies from a slice of package names. This is a backward compatibility helper for code that only has names.

type PackageJSONDetector

type PackageJSONDetector struct{}

PackageJSONDetector detects dependency sections in package.json files.

func (*PackageJSONDetector) DetectSections

func (d *PackageJSONDetector) DetectSections(content string) []SourceRange

func (*PackageJSONDetector) Supports

func (d *PackageJSONDetector) Supports(filename string) bool

type PackageLockJSONDetector

type PackageLockJSONDetector struct{}

PackageLockJSONDetector detects package entries in package-lock.json files.

func (*PackageLockJSONDetector) DetectSections

func (d *PackageLockJSONDetector) DetectSections(content string) []SourceRange

func (*PackageLockJSONDetector) Supports

func (d *PackageLockJSONDetector) Supports(filename string) bool

type PackageRef

type PackageRef struct {
	// Name is the package name as it appears in the registry.
	Name string

	// Version is the specific version being referenced.
	Version string

	// ProjectURLs contains URL mappings from the package registry, typically
	// including "repository", "homepage", "documentation", etc. The keys
	// depend on the registry. May be nil or empty.
	ProjectURLs map[string]string

	// HomePage is the project's home page URL, if available. May be empty.
	HomePage string

	// ManifestFile is the associated manifest type (e.g., "poetry", "cargo")
	// when the package comes from manifest parsing. Empty for registry-only packages.
	ManifestFile string
}

PackageRef identifies a package for metadata enrichment lookups.

It contains the information metadata providers need to look up external data like GitHub repository statistics. Created by Package.Ref.

type PackageURLs

type PackageURLs struct {
	// ProjectURLs contains URL mappings from the package registry, typically
	// including "repository", "homepage", "documentation", etc.
	ProjectURLs map[string]string

	// HomePage is the project's home page URL, if available.
	HomePage string
}

PackageURLs contains repository URL information for a package. Used by URLProvider to return URL data for manifest enrichment.

type PackagistURLProvider

type PackagistURLProvider struct {
	// contains filtered or unexported fields
}

PackagistURLProvider fetches package URLs from Packagist (PHP) in parallel. The underlying packagist.Client handles rate limiting.

func NewPackagistURLProvider

func NewPackagistURLProvider(c cache.Cache, cacheTTL time.Duration) *PackagistURLProvider

NewPackagistURLProvider creates a new Packagist URL provider.

func (*PackagistURLProvider) FetchURLs

func (p *PackagistURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given package names from Packagist. Packages are processed in chunks of 50 for better progress feedback.

type PnpmLockDetector

type PnpmLockDetector struct{}

PnpmLockDetector detects package entries in pnpm-lock.yaml files.

func (*PnpmLockDetector) DetectSections

func (d *PnpmLockDetector) DetectSections(content string) []SourceRange

func (*PnpmLockDetector) Supports

func (d *PnpmLockDetector) Supports(filename string) bool

type PoetryLockDetector

type PoetryLockDetector struct{}

PoetryLockDetector detects package sections in poetry.lock files.

func (*PoetryLockDetector) DetectSections

func (d *PoetryLockDetector) DetectSections(content string) []SourceRange

func (*PoetryLockDetector) Supports

func (d *PoetryLockDetector) Supports(filename string) bool

type PubGrubResolver

type PubGrubResolver struct {
	// contains filtered or unexported fields
}

PubGrubResolver implements dependency resolution using the PubGrub algorithm. This provides proper SAT-solver-based resolution with conflict detection and backtracking, producing accurate dependency graphs.

func NewPubGrubResolver

func NewPubGrubResolver(name string, fetcher Fetcher, parser ConstraintParser) (*PubGrubResolver, error)

NewPubGrubResolver creates a resolver using the PubGrub algorithm.

The fetcher must implement VersionLister for listing available versions. The parser converts ecosystem-specific constraints to PubGrub format.

Returns an error if the fetcher doesn't support version listing.

Example
package main

import (
	"fmt"
)

func main() {
	// NewPubGrubResolver wraps a Fetcher with SAT-solver-based resolution.
	// In real usage, you would pass a registry-specific fetcher and
	// constraint parser:
	//
	//   resolver, err := deps.NewPubGrubResolver("pypi", fetcher, parser)
	//   g, err := resolver.Resolve(ctx, "requests", deps.Options{})
	//
	// The resolver uses PubGrub for proper version constraint solving
	// with conflict detection and backtracking, then enriches metadata
	// in parallel.

	fmt.Println("NewPubGrubResolver creates a SAT-solver dependency resolver")
}
Output:
NewPubGrubResolver creates a SAT-solver dependency resolver

func (*PubGrubResolver) Fetch

func (r *PubGrubResolver) Fetch(ctx context.Context, name string, refresh bool) (*Package, error)

Fetch delegates to the underlying Fetcher, satisfying the Fetcher interface.

func (*PubGrubResolver) FetchVersion

func (r *PubGrubResolver) FetchVersion(ctx context.Context, name, version string, refresh bool) (*Package, error)

FetchVersion delegates to the underlying Fetcher, satisfying the Fetcher interface.

func (*PubGrubResolver) ListVersions

func (r *PubGrubResolver) ListVersions(ctx context.Context, name string, refresh bool) ([]string, error)

ListVersions delegates to the underlying VersionLister, satisfying the VersionLister interface so callers can list versions through the resolver.

func (*PubGrubResolver) ListVersionsWithConstraints

func (r *PubGrubResolver) ListVersionsWithConstraints(ctx context.Context, name string, refresh bool) (map[string]string, error)

ListVersionsWithConstraints delegates to the underlying fetcher if it implements RuntimeConstraintLister. Returns nil, nil if not supported.

func (*PubGrubResolver) Name

func (r *PubGrubResolver) Name() string

Name returns the resolver identifier.

func (*PubGrubResolver) ProbeRuntimeConstraint

func (r *PubGrubResolver) ProbeRuntimeConstraint(ctx context.Context, name, version string, refresh bool) (RuntimeConstraintProbe, error)

ProbeRuntimeConstraint probes runtime requirements for a package. It fetches either latest or a specific version and returns normalized runtime metadata.

func (*PubGrubResolver) Resolve

func (r *PubGrubResolver) Resolve(ctx context.Context, pkg string, opts Options) (*dag.DAG, error)

Resolve uses PubGrub to resolve the dependency graph.

type PyPIURLProvider

type PyPIURLProvider struct {
	// contains filtered or unexported fields
}

PyPIURLProvider fetches package URLs from PyPI in parallel. The underlying pypi.Client handles rate limiting (50 req/s, burst 30).

func NewPyPIURLProvider

func NewPyPIURLProvider(c cache.Cache, cacheTTL time.Duration) *PyPIURLProvider

NewPyPIURLProvider creates a new PyPI URL provider.

func (*PyPIURLProvider) FetchURLs

func (p *PyPIURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given package names from PyPI. Packages are processed in chunks of 50 for better progress feedback.

type PyprojectDetector

type PyprojectDetector struct{}

PyprojectDetector detects dependency sections in pyproject.toml files.

func (*PyprojectDetector) DetectSections

func (d *PyprojectDetector) DetectSections(content string) []SourceRange

func (*PyprojectDetector) Supports

func (d *PyprojectDetector) Supports(filename string) bool

type RequirementsDetector

type RequirementsDetector struct{}

RequirementsDetector detects dependency lines in requirements.txt files.

func (*RequirementsDetector) DetectSections

func (d *RequirementsDetector) DetectSections(content string) []SourceRange

func (*RequirementsDetector) Supports

func (d *RequirementsDetector) Supports(filename string) bool

type Resolver

type Resolver interface {
	// Resolve fetches the package and its transitive dependencies.
	//
	// Starting from pkg, the resolver recursively fetches dependencies up to
	// Options.MaxDepth levels deep and Options.MaxNodes total packages.
	//
	// Returns a [dag.DAG] where:
	//   - Nodes represent packages (ID = package name)
	//   - Edges represent dependencies (From depends on To)
	//   - Node.Meta contains package metadata from [Package.Metadata] and
	//     enrichment from Options.MetadataProviders
	//
	// The DAG is fully connected from the root package. Isolated nodes may
	// appear if dependency fetching fails for non-root packages.
	//
	// Returns an error if:
	//   - The root package cannot be fetched (registry error or does not exist)
	//   - The context is canceled
	//   - Internal errors occur
	//
	// Partial failures (missing transitive dependencies) are logged via
	// Options.Logger but do not fail the entire resolution.
	//
	// Resolve is safe for concurrent use if the underlying Fetcher is safe.
	Resolve(ctx context.Context, pkg string, opts Options) (*dag.DAG, error)

	// Name returns the resolver's identifier (e.g., "pypi", "npm", "crates").
	//
	// This is used for logging and error messages.
	Name() string
}

Resolver builds a dependency graph starting from a root package.

The sole production implementation is PubGrubResolver, which uses a SAT-solver for proper constraint resolution with backtracking.

type RubyGemsURLProvider

type RubyGemsURLProvider struct {
	// contains filtered or unexported fields
}

RubyGemsURLProvider fetches package URLs from RubyGems in parallel. The underlying rubygems.Client handles rate limiting (30 req/s, burst 20).

func NewRubyGemsURLProvider

func NewRubyGemsURLProvider(c cache.Cache, cacheTTL time.Duration) *RubyGemsURLProvider

NewRubyGemsURLProvider creates a new RubyGems URL provider.

func (*RubyGemsURLProvider) FetchURLs

func (p *RubyGemsURLProvider) FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)

FetchURLs fetches repository URLs for the given gem names from RubyGems. Packages are processed in chunks of 50 for better progress feedback.

type RuntimeConstraintLister

type RuntimeConstraintLister interface {
	// ListVersionsWithConstraints returns a map of version -> runtime constraint
	// (e.g., ">=3.8" for Python) for all versions of a package.
	// Empty string values indicate no runtime constraint for that version.
	//
	// Returns an error if:
	//   - The package does not exist in the registry
	//   - The registry API is unreachable or returns an error
	ListVersionsWithConstraints(ctx context.Context, name string, refresh bool) (map[string]string, error)
}

RuntimeConstraintLister extends Fetcher with the ability to list runtime constraints for all versions in a single API call. This is more efficient than calling FetchVersion for each version individually.

This is optional - fetchers that don't implement it will fall back to individual FetchVersion calls for runtime constraint information.

type RuntimeConstraintProbe

type RuntimeConstraintProbe struct {
	// Constraint is the normalized runtime constraint string (e.g., ">=3.8").
	Constraint string
	// MinVersion is the extracted minimum runtime version when derivable.
	MinVersion string
}

RuntimeConstraintProbe contains runtime requirement information for a package.

type RuntimeConstraintProber

type RuntimeConstraintProber interface {
	// ProbeRuntimeConstraint returns runtime requirements for a package.
	// If version is empty, the latest package version is probed.
	ProbeRuntimeConstraint(ctx context.Context, name, version string, refresh bool) (RuntimeConstraintProbe, error)
}

RuntimeConstraintProber extends Resolver with package-level runtime probing. This lets orchestration layers infer runtime defaults without constructing registry-specific clients directly.

type SectionDetector

type SectionDetector interface {
	// DetectSections scans the manifest content and returns the line ranges
	// where dependencies are defined.
	DetectSections(content string) []SourceRange

	// Supports reports whether this detector handles the given filename.
	Supports(filename string) bool
}

SectionDetector detects dependency sections in manifest content. Each language implements this interface to find where dependencies are declared.

func DefaultDetectors

func DefaultDetectors() []SectionDetector

DefaultDetectors returns the standard set of section detectors for all supported languages.

type SourceRange

type SourceRange struct {
	// StartLine is the 1-indexed line number where the section starts.
	StartLine int `json:"start_line"`

	// EndLine is the 1-indexed line number where the section ends (inclusive).
	EndLine int `json:"end_line"`

	// Section is a human-readable identifier for the section (e.g., "project.dependencies").
	Section string `json:"section"`
}

SourceRange identifies a section of a manifest file where dependencies are defined. This is used to highlight the relevant sections in the frontend viewer.

func DetectDependencySections

func DetectDependencySections(content, filename string, detectors ...SectionDetector) []SourceRange

DetectDependencySections finds dependency sections in a manifest file. It tries each detector in order and returns results from the first match. Returns nil if no detector supports the filename.

type URLProvider

type URLProvider interface {
	// FetchURLs returns repository URLs for the given package names.
	// Implementations should fetch in parallel and respect rate limits.
	// Returns a map from package name to URLs (ProjectURLs map and HomePage).
	// Packages not found or without URLs are omitted from the result.
	FetchURLs(ctx context.Context, names []string, refresh bool) (map[string]PackageURLs, error)
}

URLProvider fetches repository URLs for packages from a registry. Used by EnrichGraph to populate PackageRef.ProjectURLs when parsing manifest files. This enables GitHub enrichment for lock files and other manifests that don't include repository URLs directly.

func NewURLProvider

func NewURLProvider(language string, c cache.Cache, cacheTTL time.Duration) URLProvider

NewURLProvider returns the ecosystem URL provider for the given language. Returns nil for unsupported languages.

type UvLockDetector

type UvLockDetector struct{}

UvLockDetector detects package sections in uv.lock files.

func (*UvLockDetector) DetectSections

func (d *UvLockDetector) DetectSections(content string) []SourceRange

func (*UvLockDetector) Supports

func (d *UvLockDetector) Supports(filename string) bool

type VersionHinter

type VersionHinter interface {
	// HintedVersion extracts an exact version string from a constraint that
	// should be hinted to the version lister. Returns empty string if no
	// version should be hinted. This allows PubGrub to find versions that
	// aren't listed by the registry (e.g., Go pseudo-versions).
	HintedVersion(constraint string) string
}

VersionHinter is an optional interface that ConstraintParser implementations can implement to provide version hints from constraints. This is needed for ecosystems like Go where constraints reference exact versions (pseudo-versions) that may not appear in the registry's version list.

type VersionLister

type VersionLister interface {
	// ListVersions returns all available versions for a package, sorted from
	// oldest to newest. Pre-release versions (alpha, beta, rc, dev) may be
	// included but are typically filtered out during constraint resolution.
	//
	// Returns an error if:
	//   - The package does not exist in the registry
	//   - The registry API is unreachable or returns an error
	ListVersions(ctx context.Context, name string, refresh bool) ([]string, error)
}

VersionLister extends Fetcher with the ability to list available versions. This is optional - fetchers that don't implement it will fall back to latest version resolution for transitive dependencies.

type YarnLockDetector

type YarnLockDetector struct{}

YarnLockDetector detects package entries in yarn.lock files.

func (*YarnLockDetector) DetectSections

func (d *YarnLockDetector) DetectSections(content string) []SourceRange

func (*YarnLockDetector) Supports

func (d *YarnLockDetector) Supports(filename string) bool

Directories

Path Synopsis
Package golang provides dependency resolution for Go modules.
Package golang provides dependency resolution for Go modules.
Package java provides dependency resolution for Maven/Java packages.
Package java provides dependency resolution for Maven/Java packages.
Package javascript provides dependency resolution for npm packages.
Package javascript provides dependency resolution for npm packages.
Package languages provides the complete list of supported language ecosystems.
Package languages provides the complete list of supported language ecosystems.
Package metadata provides metadata enrichment from external sources.
Package metadata provides metadata enrichment from external sources.
Package php provides dependency resolution for PHP Composer packages.
Package php provides dependency resolution for PHP Composer packages.
Package python provides dependency resolution for Python packages.
Package python provides dependency resolution for Python packages.
Package ruby provides dependency resolution for Ruby gems.
Package ruby provides dependency resolution for Ruby gems.
Package rust provides dependency resolution for Rust crates.
Package rust provides dependency resolution for Rust crates.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL