Skip to content

feat(rules)!: rename release-age to no-outdated-packages - #190

Merged
Gallevy merged 2 commits into
mainfrom
claude/release-age-rule-naming-f52acc
Sep 13, 2026
Merged

Gallevy merged 2 commits into
mainfrom
claude/release-age-rule-naming-f52acc

Conversation

@Gallevy

@Gallevy Gallevy commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

Renames the release-age rule to no-outdated-packages, with the old key still working behind a deprecation warning.

Rebased onto ec7ba75 (#187).

Why

Every other rule id names a policy — no-files, require-files, max-file-size, no-packages, require-engine-version, require-codeowners. release-age was the only one naming the thing being measured instead.

#187 sharpened this: it landed no-deprecated-packages as the other registry-backed rule. The two read as a pair now — no-deprecated-packages / no-outdated-packages — where before one of them was the odd one out.

The stronger reason is that the old name was a false friend. minimumReleaseAge (Renovate, pnpm 10.16+), Dependabot's "cooldown", and npm's --before all mean don't install anything younger than N days — supply-chain safety. This rule is the exact inverse: don't stay behind an upgrade that has already aged past your threshold. Same units, opposite direction. Someone arriving from Renovate read release-age: { thresholds: { patch: 30 } } and concluded "don't take patches younger than 30 days" — backwards, with nothing in the name to correct them.

Migration

 rules: {
-  'release-age': [{ severity: 'error', patterns: ['@my-org/*'] }],
+  'no-outdated-packages': [{ severity: 'error', patterns: ['@my-org/*'] }],
 }

rules['release-age'] still works and behaves identically, in both the base rules block and inside overrides[].rules. It warns on load:

⚠ `rules['release-age']` is deprecated — rename it to `rules['no-outdated-packages']`. The old key still works for now and behaves identically.

Warnings go to stderr, so --format json on stdout stays clean.

Authoring both spellings is an error, not a silent precedence rule — the two would resolve to one governing entry per package, and quietly picking a winner would hide a policy decision the user never made.

How

Deprecated keys are migrated in a new src/config/deprecations.ts before Zod sees the config. That's what lets HermexConfigSchema stay .strict(): the schema describes only the current vocabulary, and exactly one module knows the old one. Retiring the alias later is a one-line deletion there, not a schema change.

Scope

Renamed:

  • config key rules['release-age'] → rules['no-outdated-packages'] (base + overrides)
  • emitted ruleId — breaking for --json consumers
  • ReleaseAgeViolation → NoOutdatedPackagesViolation
  • src/rules/release-age.ts → src/rules/no-outdated-packages.ts, evaluateReleaseAge → evaluateOutdatedPackages

Deliberately not renamed:

  • Internal field names (worstLevel, UpgradeLevel, minCompliant*, installedVersion). These belong to refactor(release-age): separate registry facts, rule verdict, and display state #189, which will move them between layers rather than rename them in place — renaming here would be churn we'd undo.
  • The releaseAge config block (authToken/cacheTtlMs/cacheDisabled) and the enricher's internals. That's registry connection/infra shared with no-deprecated-packages, not rule policy.
  • Fixture case ids and fixture config filenames (comply-release-age, configs/release-age.config.ts). Test scaffolding; renaming them would make every case look new to test:output's against-main comparison for no reviewer benefit.

Docs

The rule's section is retitled, and picks up a disclaimer that the familiar word otherwise invites:

Not the same set as npm outdated. npm outdated lists a package the moment anything newer is published. This rule deliberately waits: a newer release only counts once it has aged past the threshold for its bump tier, so a patch published yesterday is not a violation.

Plus a migration note. Both docs/examples.md and docs-templates/__examples.md are updated, since which one is the source of truth is currently ambiguous (see plan 034).

Verification

  • pnpm run typecheck / pnpm run lint / pnpm run format — clean
  • pnpm run test:ci — 1171 passed, 56 files. 7 new: 5 on the migration rules, 2 on the loadConfig seam (the warning path had no coverage otherwise)
  • End-to-end: ran the built CLI against a config using the old key — warning fires on stderr, rule runs, verdict unchanged
  • Output review: 1 of 28 cases changed — comply-all-rule-types-json, +2 −2, which is the ruleId string and nothing else. 0 invariant breaches.

Needs the output:approved label, since the JSON ruleId change is a real (intended) output change.

Follow-up

#189 — the layering fix. This rule is the only one whose verdict is computed in the registry layer and stored on the display object rather than going facts → rule → violations → display. That's what makes fields like worstLevel and minCompliantVersion hard to name honestly, and it's why this PR leaves them alone.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 3a5b691

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
hermex Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Every other rule id names a policy — `no-files`, `require-files`,
`max-file-size`, `no-packages`, `require-engine-version`. `release-age` was
the only one naming the thing being measured instead. With #187 landing
`no-deprecated-packages`, the registry-backed family now reads
`no-deprecated-packages` / `no-outdated-packages` as a pair.

It also collided with ecosystem vocabulary in the worst direction:
`minimumReleaseAge` (Renovate, pnpm) and npm's `--before` all mean "don't
install anything younger than N days". This rule is the inverse — don't stay
behind an upgrade that has already aged past your threshold — so the old name
inverted the polarity for anyone arriving from those tools.

`rules['release-age']` still works, in both the base `rules` block and inside
`overrides[].rules`, and warns on load. Deprecated keys are migrated in a new
`src/config/deprecations.ts` before Zod sees the config, which is what lets
`HermexConfigSchema` stay `.strict()` — the schema describes only the current
vocabulary and one module knows the old one.

Authoring both spellings is an error rather than a silent precedence rule:
the two would resolve to one governing entry per package, and quietly picking
a winner would hide a policy decision the user never made.

Internal field names (`worstLevel`, `UpgradeLevel`, `minCompliant*`) are
deliberately untouched — they belong to the layering fix in #189, which will
move rather than rename them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Gallevy
Gallevy force-pushed the claude/release-age-rule-naming-f52acc branch from a1fccb2 to a425977 Compare September 13, 2026 16:49
@github-actions

Copy link
Copy Markdown
Contributor

Output Review

1 of 28 case(s) changed · 0 invariant breach(es) · all 28 cases

Reference: ec7ba75 — reused from cache.

Case Change
comply-all-rule-types-json +2 −2 case →

Open a case to read its diff, its config and its full output. If this is the change you meant, apply the output:approved label — see CONTRIBUTING.md.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 99.19%
⬆️ +0.0%
1735 / 1749
🟢 Statements 98.66%
⬆️ +0.0%
1927 / 1953
🟢 Functions 98.97%
⬆️ +0.0%
386 / 390
🟢 Branches 97.66%
⬆️ +0.0%
1337 / 1369
File Coverage
File Stmts Branches Functions Lines
Changed Files
src/utils/print-rules.ts 93%
🟰 ±0%
91.35%
🟰 ±0%
100%
🟰 ±0%
94.56%
🟰 ±0%
src/npm-registry/enricher.ts 95.07%
🟰 ±0%
90.37%
🟰 ±0%
100%
🟰 ±0%
99.16%
🟰 ±0%
src/utils/print-packages.ts 99%
🟰 ±0%
98.43%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/config/deprecations.ts 100% 100% 100% 100%
src/config/loader.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/config/overrides.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/config/schema.ts 100%
🟰 ±0%
96.55%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/rules/shared.ts 100%
🟰 ±0%
94.11%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/compliance.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/package-distribution.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/package-flags.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/package-rules.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/severity-format.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/utils/write-summary-file.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
tests/helpers/mock-reports.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
Generated in workflow #240 for commit 3a5b691 by the Vitest Coverage Report Action

@Gallevy Gallevy added the output:approved You read the output-review diff and confirmed it label Sep 13, 2026
The migration runs inside `loadConfig` before Zod, so the warning path had
no automated coverage — only `migrateDeprecatedConfig` in isolation did.
Pins that a legacy `release-age` config resolves onto
`no-outdated-packages` and warns exactly once, on stderr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the output:approved You read the output-review diff and confirmed it label Sep 13, 2026
@Gallevy
Gallevy enabled auto-merge (squash) September 13, 2026 16:52
@Gallevy Gallevy added the output:approved You read the output-review diff and confirmed it label Sep 13, 2026
@Gallevy
Gallevy merged commit fda8b4b into main Sep 13, 2026
7 of 8 checks passed
@Gallevy
Gallevy deleted the claude/release-age-rule-naming-f52acc branch September 13, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

output:approved You read the output-review diff and confirmed it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant