go get github.com/future-architect/uzomuzo-ossThe library is built around a single high-level Evaluator that performs the following in one call:
- Data collection (deps.dev, GitHub, registry metadata)
- Lifecycle heuristics (activity / release freshness)
- Primary source EOL integration (registry deprecation signals / successor detection)
package main
import (
"context"
"fmt"
"os"
"github.com/future-architect/uzomuzo-oss/pkg/uzomuzo"
)
func main() {
ctx := context.Background()
// Create full evaluator (GitHub token strongly recommended for commit-based assessment precision)
client := uzomuzo.NewEvaluator(os.Getenv("GITHUB_TOKEN"))
// Full evaluation (includes lifecycle heuristics + EOL / successor integration)
analyses, err := client.EvaluatePURLs(ctx, []string{
"pkg:npm/express@4.18.2",
"pkg:pypi/requests@2.28.1",
})
if err != nil {
panic(err)
}
for purl, a := range analyses {
if a.Error != nil {
fmt.Printf("%s: error: %v\n", purl, a.Error)
continue
}
fmt.Printf("\nPackage: %s\n", a.DisplayPURL())
fmt.Printf("Overall Score: %.1f/10 SecPolicy: %.1f Vulns: %.1f\n",
a.OverallScore,
a.GetScore("Security-Policy"),
a.GetScore("Vulnerabilities"),
)
stars, forks := 0, 0
if a.Repository != nil { stars, forks = a.Repository.StarsCount, a.Repository.ForksCount }
fmt.Printf("Repo: archived=%t disabled=%t stars=%d forks=%d\n", a.IsArchived(), a.IsDisabled(), stars, forks)
// Lifecycle axis (optional)
if lr := a.GetLifecycleResult(); lr != nil {
fmt.Printf("Lifecycle: %s (%s)\n", lr.Label, lr.Reason)
}
// Project-level license (single ResolvedLicense)
proj := a.ProjectLicense
switch {
case proj.IsZero():
fmt.Println("Project License: (absent)")
case proj.IsNonStandard():
fmt.Printf("Project License: non-standard raw=%q source=%s\n", proj.Raw, proj.Source)
default:
fmt.Printf("Project License: SPDX=%s raw=%q source=%s\n", proj.Identifier, proj.Raw, proj.Source)
}
// Simple health rule (customize freely in your app)
healthy := a.OverallScore >= 7.0 && a.GetScore("Security-Policy") >= 8.0 && !a.IsArchived()
fmt.Printf("Healthy? %t\n", healthy)
}
}Key points of the example above:
- Integrates fetch (licenses, repo metadata, scorecard) with lifecycle evaluation
- Uses intention helpers for license inspection instead of branching on
Sourceconstants directly - Deterministic fallback / promotion is already applied within the returned
Analysisobject - Callers define their own "health" criteria — the library provides raw signals & lifecycle axis but no opinionated judgments
When embedding the library, use the stable helpers:
finalEn := analysis.EOL.FinalReason() // English
finalJa := analysis.EOL.FinalReasonJa() // Japanese (if available); falls back to EnglishSelection priority within FinalReason():
- Evidence with highest Confidence (ties: first)
- First evidence (fallback)
- Empty string (no evidence/reason available)
Centralizing this logic keeps UI / export layers deterministic. Avoid re-implementing the ordering on the client side.
NewEvaluator(token, opts...)— Build a full evaluation client (acceptsWithEnricheroptions). Token enables commit history and Scorecard data for higher assessment precision (see Assessment Precision)EvaluatePURLs(ctx, purls)— Full PURL evaluation (score + lifecycle + EOL)EvaluateGitHubRepos(ctx, urls)— Full evaluation for GitHub repositoriesExportCSV(results, path)— Export full metrics as CSV
Thin wrappers for fetching raw data without running the full evaluation pipeline:
FetchGitHubREADME(ctx, owner, repo, defaultBranch)— Fetch raw README text from a GitHub repository (tries README.md, README.MD, README, README.txt, README.rst)FetchPyPIProject(ctx, name)— Fetch project metadata from PyPI JSON API (returns*PyPIProjectInfo)IsGitHubURL(rawURL)— Check whether a URL points to a GitHub repositoryParseGitHubURL(rawURL)— Extract owner and repo from a GitHub URL
Note: ResolvedLicense (used by Analysis.ProjectLicense and Analysis.RequestedVersionLicenses) is defined in internal/domain/analysis and is not re-exported from pkg/uzomuzo. You can call its methods (e.g., IsZero(), IsNonStandard()) through the field, but cannot reference the type directly in your own signatures.
The Analysis type provides rich domain methods:
OverallScore— Overall scorecard score (0-10)GetScore(name)— Specific check score (-1 if not found)GetCheckMap()— Map of all scores (name → value)IsArchived()/IsDisabled()— Repository statusAxisResults— Assessment axis → result map (lifecycle axis key:"lifecycle"); useGetLifecycleResult()helper if only lifecycle is neededHasError()/GetErrorMessage()— Error handling- Direct field access to all analysis data
Users should define their own health/risk criteria rather than relying on opinionated convenience methods.
Currently only the lifecycle assessment axis is provided. Additional axes (e.g., Build Health) can be added by implementing AssessmentService and composing with NewCompositeAssessor. Place business logic in the domain layer and inject thresholds via LifecycleAssessmentConfig to maintain DDD compliance.
uzomuzo provides built-in EOL detection from registry signals (PyPI classifiers, npm deprecated, Packagist abandoned, NuGet deprecated, Maven relocated). For comprehensive EOL coverage, you can inject your own EOL catalog enricher using the WithEnricher hook.
The evaluation pipeline runs in three phases:
- Phase 1 — Registry heuristics (built-in, automatic)
- Phase 2 — Enrichers (your custom catalog logic runs here)
- Phase 3 — Lifecycle assessment (built-in, uses Phase 1+2 results)
Enrichers run between Phase 1 and Phase 3, so catalog-based EOL decisions feed into the final lifecycle label.
Your EOL catalog can use any format you prefer. Below is a recommended structure per ecosystem shard:
{
"schema_version": 2,
"generated_at": "2025-06-01T00:00:00Z",
"records": [
{
"purl": "pkg:npm/left-pad",
"judgments": {
"human": {
"status": "eol",
"reason": "Unpublished from npm in 2016 and abandoned. No updates since.",
"successor": "pkg:npm/pad-left",
"confidence": "high"
}
}
},
{
"purl": "pkg:pypi/some-future-lib",
"judgments": {
"human": {
"status": "scheduled",
"eol_date": "2026-12-31",
"reason": "Official deprecation announcement. Migrate to v2.",
"successor": "pkg:pypi/some-future-lib-v2",
"confidence": "high"
}
}
},
{
"purl": "pkg:maven/junit/junit",
"judgments": {
"human": {
"status": "not_eol",
"reason": "Still maintained. JUnit 4 receives critical fixes.",
"confidence": "medium"
}
}
}
]
}Field reference:
| Field | Required | Values |
|---|---|---|
purl |
yes | Versionless PURL (e.g., pkg:npm/express) |
judgments.human.status |
yes | eol, not_eol, scheduled, pending, unknown |
judgments.human.eol_date |
for scheduled | YYYY-MM-DD format |
judgments.human.reason |
recommended | Human-readable rationale |
judgments.human.successor |
optional | Successor PURL or URL |
judgments.human.confidence |
optional | high, medium, low |
The enricher you write is responsible for loading and matching this JSON — uzomuzo does not prescribe the loader implementation.
package main
import (
"context"
"fmt"
"os"
"github.com/future-architect/uzomuzo-oss/pkg/uzomuzo"
)
func main() {
ctx := context.Background()
evaluator := uzomuzo.NewEvaluator(
os.Getenv("GITHUB_TOKEN"),
uzomuzo.WithEnricher(myCatalogEnricher),
)
results, err := evaluator.EvaluatePURLs(ctx, []string{"pkg:npm/left-pad@1.0.0"})
if err != nil {
panic(err)
}
for purl, a := range results {
fmt.Printf("%s: maintenance=%s eol=%s\n", purl, a.FinalMaintenanceStatus(), a.EOL.HumanState())
}
}An AnalysisEnricher receives the full analysis map after Phase 1. It may set or override Analysis.EOL fields:
func myCatalogEnricher(ctx context.Context, analyses map[string]*uzomuzo.Analysis) error {
for key, a := range analyses {
if a == nil {
continue
}
// Look up PURL in your catalog (implement your own loader/matcher)
match := myCatalog.Lookup(a.EffectivePURL)
if match == nil {
continue
}
switch {
case match.IsEOL:
a.EOL = uzomuzo.EOLStatus{
State: uzomuzo.EOLEndOfLife,
Successor: match.Successor,
Reason: match.Reason,
Evidences: []uzomuzo.EOLEvidence{{
Source: "MyCatalog",
Summary: match.Reason,
Reference: match.ReferenceURL,
Confidence: 0.95,
}},
}
case match.IsScheduled:
scheduledAt := match.EOLDate
a.EOL = uzomuzo.EOLStatus{
State: uzomuzo.EOLScheduled,
ScheduledAt: &scheduledAt,
Successor: match.Successor,
Reason: match.Reason,
}
case match.IsNotEOL:
// Explicitly mark as not-EOL (overrides registry heuristic false positives)
a.EOL = uzomuzo.EOLStatus{
State: uzomuzo.EOLNotEOL,
Reason: match.Reason,
}
}
}
return nil
}- MAY set or override
Analysis.EOL(State, Successor, ScheduledAt, Reason, ReasonJa, Evidences) - MAY clear
Analysis.Error(e.g., when catalog knows about a package that deps.dev doesn't) - MUST NOT modify other aggregate fields (scores, repository metadata, etc.)
- Multiple enrichers run in the order they are registered; later enrichers can override earlier ones
All types are re-exported via pkg/uzomuzo:
| Type | Description |
|---|---|
uzomuzo.EOLStatus |
Aggregates EOL decision + evidence |
uzomuzo.EOLState |
Enum: EOLUnknown, EOLNotEOL, EOLEndOfLife, EOLScheduled |
uzomuzo.EOLEvidence |
Single evidence item (Source, Summary, Reference, Confidence) |
After enrichers run, Phase 3 converts EOLStatus into the final maintenance status:
| EOL State | Maintenance Status |
|---|---|
EOLEndOfLife |
EOL |
EOLScheduled |
Scheduled EOL |
EOLNotEOL |
(determined by activity heuristics) |
EOLUnknown |
(determined by activity heuristics) |
Access the final status via a.FinalMaintenanceStatus() or the richer uzomuzo.BuildLifecycleSummary(a).
See examples/eol_detection for a runnable demo of the full evaluation pipeline (scores, lifecycle labels, EOL evidences, successor info):
go run ./examples/eol_detection