Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,29 @@ GITTENSORY_REVIEW_ENRICHMENT=false
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting,errorSwallow,unsafeAny,a11y
# i18n,unusedExport,exhaustiveness,flakyTest,commitLint,apiBreak
# i18n,unusedExport,exhaustiveness,flakyTest,commitLint,apiBreak,deprecatedDep
#
# Profile defaults:
# fast: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild
# testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker
# debugLeftover,sizeSmell,floatingPromise,deepNesting,errorSwallow,unsafeAny,a11y,i18n,apiBreak
# deprecatedDep
# balanced (default): dependency,dependencyDiff,lockfileDrift,secret,license,installScript
# heavyDependency,hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight
# typosquat,commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication
# churnHotspot,blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch
# commitHygiene,pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology
# todoMarker,magicNumber,conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting
# errorSwallow,unsafeAny,a11y,i18n,unusedExport,exhaustiveness,flakyTest,commitLint,apiBreak
# deprecatedDep
# deep: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,churnHotspot
# blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch,commitHygiene
# pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber
# conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting,errorSwallow,unsafeAny,a11y
# i18n,unusedExport,exhaustiveness,flakyTest,commitLint,apiBreak
# i18n,unusedExport,exhaustiveness,flakyTest,commitLint,apiBreak,deprecatedDep
# END GENERATED REES ANALYZERS

# Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep
Expand Down
25 changes: 25 additions & 0 deletions apps/gittensory-ui/src/lib/rees-analyzers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,31 @@ export const REES_ANALYZERS = [
"Conservative: only a top-level export whose exact name disappears from the file's public surface is reported; a same-name edit (signature or value change) or a non-entrypoint file is never flagged. Bounded by entrypoint and finding caps; fail-safe on absent or malformed patches.",
},
},
{
name: "deprecatedDep",
title: "Deprecated / unmaintained dependency",
category: "supply-chain",
cost: "local",
defaultEnabled: true,
profiles: ["fast", "balanced", "deep"],
requires: ["files"],
limits: {
maxManifestFiles: 20,
maxPatchLinesPerFile: 500,
maxFindings: 25,
},
docs: {
summary:
"Flags a direct dependency a PR newly adds or upgrades that is an officially deprecated or unmaintained package with a maintained successor — an adoption risk the review brief should surface.",
looksAt:
"Added/changed dependency names in package.json and requirements.txt patches, matched against a bundled curated list of well-known deprecated packages.",
reports:
"Ecosystem, package, added version, direction (add/change), the documented deprecation reason, and the recommended replacement — never manifest contents.",
network: "Pure local analyzer. No external network call; the curated list is bundled.",
notes:
"Conservative: only an exact match against the bundled list is flagged, so a package it does not name is never reported. Bounded by manifest, patch-line, and finding caps; fail-safe on absent patches or an aborted signal.",
},
},
] as const satisfies readonly ReesAnalyzerDoc[];

export const REES_ANALYZER_NAMES = REES_ANALYZERS.map((analyzer) => analyzer.name);
27 changes: 27 additions & 0 deletions review-enrichment/analyzer-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,33 @@
"network": "Pure local analyzer. No external network call.",
"notes": "Conservative: only a top-level export whose exact name disappears from the file's public surface is reported; a same-name edit (signature or value change) or a non-entrypoint file is never flagged. Bounded by entrypoint and finding caps; fail-safe on absent or malformed patches."
}
},
{
"name": "deprecatedDep",
"title": "Deprecated / unmaintained dependency",
"category": "supply-chain",
"cost": "local",
"defaultEnabled": true,
"profiles": [
"fast",
"balanced",
"deep"
],
"requires": [
"files"
],
"limits": {
"maxManifestFiles": 20,
"maxPatchLinesPerFile": 500,
"maxFindings": 25
},
"docs": {
"summary": "Flags a direct dependency a PR newly adds or upgrades that is an officially deprecated or unmaintained package with a maintained successor — an adoption risk the review brief should surface.",
"looksAt": "Added/changed dependency names in package.json and requirements.txt patches, matched against a bundled curated list of well-known deprecated packages.",
"reports": "Ecosystem, package, added version, direction (add/change), the documented deprecation reason, and the recommended replacement — never manifest contents.",
"network": "Pure local analyzer. No external network call; the curated list is bundled.",
"notes": "Conservative: only an exact match against the bundled list is flagged, so a package it does not name is never reported. Bounded by manifest, patch-line, and finding caps; fail-safe on absent patches or an aborted signal."
}
}
]
}
85 changes: 85 additions & 0 deletions review-enrichment/src/analyzers/deprecated-dep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Deprecated / unmaintained direct-dependency analyzer (#1511, part of #1499). A no-checkout headless reviewer sees
// only the diff, so it cannot tell that a dependency a PR newly ADDS or UPGRADES is an officially deprecated or
// abandoned package that a maintained successor has replaced — an adoption risk + future supply-chain liability the
// review brief should surface. This fills that gap purely from the changed manifest patches: it reuses the shared
// manifest dependency-change parser and matches each added/upgraded package name against a BUNDLED, curated list of
// well-known deprecated packages per ecosystem — the same offline-list approach the typosquat analyzer uses for its
// popular-package set. Deterministic, no network, no token: the curated list is the sole source of truth, so a
// package it does not name is never flagged (conservative + fail-safe). Reports ecosystem, package, the added
// version, the change direction, the documented reason, and the recommended replacement — never manifest contents.
import type { DeprecatedDependencyFinding, EnrichRequest } from "../types.js";
import { extractDependencyChanges } from "./dependency-scan.js";

const MAX_MANIFEST_FILES = 20; // bound manifest files parsed per PR
const MAX_PATCH_LINES_PER_FILE = 500; // bound patch lines parsed per manifest
const MAX_FINDINGS = 25; // keep the brief bounded

interface DeprecationNote {
reason: string;
replacement: string | null;
}

// Curated, conservative registry of packages with a WELL-KNOWN published deprecation (npm-deprecated, a PyPI
// deprecation stub, or an officially retired project) and a maintained successor. Keyed ecosystem → normalized
// package name → note. Not exhaustive by design: only unambiguous, widely-recognized cases so a match is a real
// signal and never a guess. `replacement` is the community-recommended successor, or null when none is standard.
const DEPRECATED: Record<string, Record<string, DeprecationNote>> = {
npm: {
request: { reason: "deprecated — no longer maintained since 2020", replacement: "got or axios" },
"request-promise": { reason: "deprecated with request", replacement: "got" },
"request-promise-native": { reason: "deprecated with request", replacement: "got" },
"node-sass": { reason: "deprecated — LibSass is deprecated", replacement: "sass (Dart Sass)" },
tslint: { reason: "deprecated in favor of ESLint (2019)", replacement: "eslint + typescript-eslint" },
"gulp-util": { reason: "deprecated — the bundled utility set was unpublished", replacement: null },
istanbul: { reason: "deprecated — the project was renamed", replacement: "nyc" },
"babel-preset-es2015": { reason: "deprecated — legacy Babel 6 preset", replacement: "@babel/preset-env" },
bower: { reason: "deprecated front-end package manager", replacement: "npm or yarn" },
"phantomjs-prebuilt": { reason: "deprecated — PhantomJS is suspended", replacement: "puppeteer or playwright" },
},
PyPI: {
sklearn: { reason: "deprecated PyPI stub for scikit-learn", replacement: "scikit-learn" },
nose: { reason: "unmaintained — no Python 3.10+ support", replacement: "pytest or nose2" },
pycrypto: { reason: "unmaintained — known unpatched CVEs", replacement: "pycryptodome" },
beautifulsoup: { reason: "legacy BeautifulSoup 3, no longer maintained", replacement: "beautifulsoup4" },
distribute: { reason: "deprecated — merged back into setuptools", replacement: "setuptools" },
},
};

/** Registry lookup key for a package name. npm names are case-folded; PyPI applies PEP 503 normalization —
* lowercased, with runs of `-`, `_`, and `.` collapsed to a single `-` — so `Foo_Bar` and `foo.bar` resolve
* to the same project. Pure. */
export function normalizeName(ecosystem: string, name: string): string {
const lower = name.toLowerCase();
return ecosystem === "PyPI" ? lower.replace(/[-_.]+/g, "-") : lower;
}

/** Flag each newly-added or upgraded direct dependency the curated list marks deprecated/unmaintained. Reuses the
* shared manifest parser (which only yields deps present after the change), so removals are never flagged.
* Deterministic, no network. Returns [] on an aborted signal or when no changed manifest names a listed package;
* bounded by the manifest, patch-line, and finding caps. */
export async function scanDeprecatedDependencies(
req: EnrichRequest,
signal?: AbortSignal,
): Promise<DeprecatedDependencyFinding[]> {
if (signal?.aborted) return [];
const findings: DeprecatedDependencyFinding[] = [];
const changes = extractDependencyChanges(req.files ?? [], {
maxManifestFiles: MAX_MANIFEST_FILES,
maxPatchLinesPerFile: MAX_PATCH_LINES_PER_FILE,
});
for (const change of changes) {
if (signal?.aborted) break;
const note = DEPRECATED[change.ecosystem]?.[normalizeName(change.ecosystem, change.package)];
if (!note) continue;
findings.push({
ecosystem: change.ecosystem,
package: change.package,
version: change.to,
direction: change.from ? "change" : "add",
replacement: note.replacement,
reason: note.reason,
});
if (findings.length >= MAX_FINDINGS) break;
}
return findings;
}
37 changes: 37 additions & 0 deletions review-enrichment/src/analyzers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { scanUnusedExport } from "./unused-export.js";
import { scanExhaustivenessDrift } from "./exhaustiveness-drift.js";
import { scanFlakyTest } from "./flaky-test.js";
import { scanApiBreak } from "./api-break.js";
import { scanDeprecatedDependencies } from "./deprecated-dep.js";
import type {
AnalyzerDescriptor,
AnalyzerFn,
Expand Down Expand Up @@ -1409,6 +1410,42 @@ export const ANALYZER_DESCRIPTORS = [
},
run: (req, { signal }) => scanApiBreak(req, signal),
}),
descriptor({
name: "deprecatedDep",
title: "Deprecated / unmaintained dependency",
category: "supply-chain",
cost: "local",
defaultEnabled: true,
requires: ["files"],
limits: { maxManifestFiles: 20, maxPatchLinesPerFile: 500, maxFindings: 25 },
docs: {
summary:
"Flags a direct dependency a PR newly adds or upgrades that is an officially deprecated or unmaintained package with a maintained successor — an adoption risk the review brief should surface.",
looksAt:
"Added/changed dependency names in package.json and requirements.txt patches, matched against a bundled curated list of well-known deprecated packages.",
reports:
"Ecosystem, package, added version, direction (add/change), the documented deprecation reason, and the recommended replacement — never manifest contents.",
network: "Pure local analyzer. No external network call; the curated list is bundled.",
notes:
"Conservative: only an exact match against the bundled list is flagged, so a package it does not name is never reported. Bounded by manifest, patch-line, and finding caps; fail-safe on absent patches or an aborted signal.",
},
render: (findings, helpers) => {
if (!findings.length) return [];
const lines = [
"### Deprecated or unmaintained dependencies (newly added or upgraded)",
];
for (const item of findings) {
const replacement = item.replacement
? `; consider ${helpers.safeCodeSpan(item.replacement)}`
: "";
lines.push(
`- ${helpers.safeCodeSpan(`${item.package}@${item.version}`)} (${helpers.safeCodeSpan(item.ecosystem)}) — ${helpers.promptText(item.reason)}${replacement}`,
);
}
return lines;
},
run: (req, { signal }) => scanDeprecatedDependencies(req, signal),
}),
] as const satisfies readonly AnyAnalyzerDescriptor[];

export const ANALYZER_NAMES = ANALYZER_DESCRIPTORS.map(
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ export function renderBrief(
lines.push(...renderDescriptorSection("hardcodedUrl", findings.hardcodedUrl));
lines.push(...renderDescriptorSection("commitLint", findings.commitLint));
lines.push(...renderDescriptorSection("apiBreak", findings.apiBreak));
lines.push(...renderDescriptorSection("deprecatedDep", findings.deprecatedDep));

if (!lines.length) return { promptSection: "", systemSuffix: "" };

Expand Down
13 changes: 13 additions & 0 deletions review-enrichment/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,18 @@ export interface CommitLintFinding {
reason: "bad-type" | "missing-colon" | "too-long" | "empty";
}

/** A newly-added or upgraded direct dependency the curated list marks deprecated/unmaintained, with a maintained
* successor where one exists — adoption risk + future supply-chain liability (#1511, part of #1499). Reports the
* ecosystem, package, added version, direction, reason, and replacement only — never manifest contents. */
export interface DeprecatedDependencyFinding {
ecosystem: string;
package: string;
version: string;
direction: "add" | "change";
replacement: string | null;
reason: string;
}

/** Structured analyzer output. Each analyzer fills its own key; more land as analyzers ship (#1477/#1478). */
/** An exported symbol a PR removes or renames in a package public entrypoint — a semver-major break for
* downstream consumers shipped without a major version bump (#1510, part of #1499). Reports file, old-file line,
Expand Down Expand Up @@ -652,6 +664,7 @@ export interface BriefFindings {
hardcodedUrl?: HardcodedUrlFinding[];
commitLint?: CommitLintFinding[];
apiBreak?: ApiBreakFinding[];
deprecatedDep?: DeprecatedDependencyFinding[];
}

/** A JSDoc/TSDoc block whose `@param` tags name parameters the adjacent function no longer declares — a
Expand Down
1 change: 1 addition & 0 deletions review-enrichment/test/analyzer-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const EXPECTED_ANALYZERS = [
"flakyTest",
"commitLint",
"apiBreak",
"deprecatedDep",
];

test("analyzer descriptors cover the runtime registry in stable order", () => {
Expand Down
Loading
Loading