Summary
In internal/domain/analysis/lifecycle_assessor.go, the archive/disable branch of assessInternal returns EOL-Confirmed for any archived or disabled repository, without checking whether the package is still published on its registry. This produces false EOL classifications for packages whose repository was archived (e.g., monorepo consolidation) but whose package (PURL) is still actively published.
Where
// internal/domain/analysis/lifecycle_assessor.go (assessInternal)
// 1. Archive/disable check
if analysis != nil && (analysis.IsArchived() || analysis.IsDisabled()) {
...
return &AssessmentResult{
Axis: LifecycleAxis, Label: string(LabelEOLConfirmed),
Reason: "Repository archived or disabled", ...}, nil
}
// 1.5 Primary-source EOL status override
if in.EOL.IsEOL() { ... }
The archive check runs before the primary-source EOL check (in.EOL.IsEOL()), so for an archived repo the assessor short-circuits to EOL-Confirmed regardless of registry signals.
Impact
FinalMaintenanceStatus() returns EOL-Confirmed via this lifecycle label even when there is no primary-source EOL signal (no npm deprecated, PyPI yanked, Packagist abandoned, Maven <relocation>). Consumers that treat EOL-Confirmed as a decisive EOL signal then mis-classify actively-maintained packages as end-of-life.
Real-world examples — repository archived (monorepo consolidation) but the package is still publishing new versions:
| PURL |
repository (currently archived) |
still publishing? |
pkg:npm/google-auth-library |
googleapis/google-auth-library-nodejs |
yes |
pkg:npm/google-gax |
googleapis/gax-nodejs |
yes |
pkg:golang/github.com/grafana/k6deps |
grafana/k6deps |
yes |
For each of these, IsArchived() == true, EOL.IsEOL() == false, yet FinalMaintenanceStatus() == EOL-Confirmed (verified against the live pipeline with Evaluator.EvaluatePURLs).
Proposed fix
Gate the archive/disable → EOL-Confirmed branch on registry liveness, so EOL-Confirmed is reserved for genuine end-of-life. For example:
- If
ReleaseInfo.StableVersion exists, is not deprecated, and was published recently, do not return EOL-Confirmed from the archive branch — fall through to the normal lifecycle assessment (so the package lands on Stalled / Legacy-Safe instead).
- Keep
EOL-Confirmed for primary-source EOL (EOL.IsEOL()) and registry-declared deprecation; classify "repository archived but still published" as Stalled (dormant, not EOL).
Either approach keeps real EOL (deprecated / yanked / abandoned / relocation) as EOL-Confirmed while removing the false positives for archived-but-alive packages.
Workaround in place downstream
A downstream consumer currently compensates by downgrading EOL-Confirmed && !EOL.IsEOL() && (IsArchived() || IsDisabled()) to Stalled at ingestion time. That relies on the invariant that the lifecycle axis only emits EOL-Confirmed for archived repos when !EOL.IsEOL() (the archive check precedes the primary-source check). A registry-liveness gate in the assessor would make this downstream workaround unnecessary and fix the classification at the source.
Summary
In
internal/domain/analysis/lifecycle_assessor.go, the archive/disable branch ofassessInternalreturnsEOL-Confirmedfor any archived or disabled repository, without checking whether the package is still published on its registry. This produces false EOL classifications for packages whose repository was archived (e.g., monorepo consolidation) but whose package (PURL) is still actively published.Where
The archive check runs before the primary-source EOL check (
in.EOL.IsEOL()), so for an archived repo the assessor short-circuits toEOL-Confirmedregardless of registry signals.Impact
FinalMaintenanceStatus()returnsEOL-Confirmedvia this lifecycle label even when there is no primary-source EOL signal (no npmdeprecated, PyPIyanked, Packagistabandoned, Maven<relocation>). Consumers that treatEOL-Confirmedas a decisive EOL signal then mis-classify actively-maintained packages as end-of-life.Real-world examples — repository archived (monorepo consolidation) but the package is still publishing new versions:
pkg:npm/google-auth-librarygoogleapis/google-auth-library-nodejspkg:npm/google-gaxgoogleapis/gax-nodejspkg:golang/github.com/grafana/k6depsgrafana/k6depsFor each of these,
IsArchived() == true,EOL.IsEOL() == false, yetFinalMaintenanceStatus() == EOL-Confirmed(verified against the live pipeline withEvaluator.EvaluatePURLs).Proposed fix
Gate the archive/disable →
EOL-Confirmedbranch on registry liveness, soEOL-Confirmedis reserved for genuine end-of-life. For example:ReleaseInfo.StableVersionexists, is not deprecated, and was published recently, do not returnEOL-Confirmedfrom the archive branch — fall through to the normal lifecycle assessment (so the package lands onStalled/Legacy-Safeinstead).EOL-Confirmedfor primary-source EOL (EOL.IsEOL()) and registry-declared deprecation; classify "repository archived but still published" asStalled(dormant, not EOL).Either approach keeps real EOL (deprecated / yanked / abandoned / relocation) as
EOL-Confirmedwhile removing the false positives for archived-but-alive packages.Workaround in place downstream
A downstream consumer currently compensates by downgrading
EOL-Confirmed && !EOL.IsEOL() && (IsArchived() || IsDisabled())toStalledat ingestion time. That relies on the invariant that the lifecycle axis only emitsEOL-Confirmedfor archived repos when!EOL.IsEOL()(the archive check precedes the primary-source check). A registry-liveness gate in the assessor would make this downstream workaround unnecessary and fix the classification at the source.