arol-ai 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -10
- package/dist/cli.js +28 -13
- package/dist/data.js +9 -1
- package/dist/report.js +42 -46
- package/dist/scanner.js +6 -5
- package/dist/status.js +45 -0
- package/package.json +1 -1
- package/src/data/deprecations.json +23 -18
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Detection keys on **actual usage, not mere SDK presence.** Each dataset entry de
|
|
|
72
72
|
Flags **only** when your code actually references the deprecated API in a scanned **source file**. `detect.sdk` is just a scope hint here and is **never** a trigger on its own. A `pattern` entry carries two kinds of usage signal:
|
|
73
73
|
|
|
74
74
|
- **`detect.patterns`** — raw regexes for code identifiers, endpoints, and params (e.g. `beta\.assistants`, `/v1/threads`, `charges\.create`, `hapikey\s*=`). Matched anywhere in the file.
|
|
75
|
-
- **`detect.models`** — model family names matched **only inside a string literal**. Each becomes: an opening quote (`'` `"` or `` ` ``), the family name, an optional
|
|
75
|
+
- **`detect.models`** — model family names matched **only inside a string literal**. Each becomes: an opening quote (`'` `"` or `` ` ``), the family name, an **optional ISO date snapshot** (`-YYYY-MM-DD`), then the matching closing quote — no arbitrary trailing characters. So `"gpt-4o"`, `'gpt-4o'`, `` `gpt-4o` ``, and `"gpt-4o-2024-05-13"` match, but a *different* model like `"gpt-4o-mini"` or `"gpt-4o-realtime-preview"` does **not** (and neither does a bare mention in prose, JSX, or a comment).
|
|
76
76
|
|
|
77
77
|
This split is what keeps a marketing page that mentions *"GPT-4o, GPT-4.1, and o4-mini"* from being reported as deprecated usage: those names aren't quoted string literals, so `detect.models` ignores them. Only something like `model: "o4-mini"` counts.
|
|
78
78
|
|
|
@@ -117,20 +117,43 @@ arol-ai scan [path] [options]
|
|
|
117
117
|
| `--no-color` | Disable colored output (also respects `NO_COLOR`) |
|
|
118
118
|
| `--data <file>` | Use a custom `deprecations.json` instead of the bundled one |
|
|
119
119
|
| `--ignore <glob>` | Skip files matching this glob; repeatable. Combined with `.arolignore`. e.g. `--ignore 'docs/**' --ignore '**/*.gen.ts'` |
|
|
120
|
-
| `--
|
|
120
|
+
| `--within <days>` | Window (default `30`) for the CI gate's "scheduled soon" check. See exit codes. |
|
|
121
121
|
| `-v, --version` | Print the version |
|
|
122
122
|
| `-h, --help` | Show help |
|
|
123
123
|
|
|
124
124
|
`scan` is the default command, so `arol-ai ./repo` works too.
|
|
125
125
|
|
|
126
|
-
**Exit codes:** `0` success · `1`
|
|
126
|
+
**Exit codes:** `0` success · `1` an actionable finding · `2` bad path or dataset error.
|
|
127
|
+
|
|
128
|
+
A finding is **actionable** (exit `1`) when it is **high**-severity, or **scheduled** to sunset within `--within` days (default 30). Dateless `deprecated` findings and non-imminent `medium`/`low` findings are **warn-only** (still printed, but exit `0`). This makes `arol-ai` a sensible CI gate with no flags:
|
|
127
129
|
|
|
128
130
|
```sh
|
|
129
|
-
npx arol-ai scan
|
|
131
|
+
npx arol-ai scan # fails CI on high or imminently-scheduled findings
|
|
132
|
+
npx arol-ai scan --within 7 # only fail when a sunset is within a week
|
|
130
133
|
```
|
|
131
134
|
|
|
132
135
|
Colors are automatically disabled when output is not a TTY (e.g. piped to a file), or when `NO_COLOR` is set. Use `FORCE_COLOR=1` to force them on.
|
|
133
136
|
|
|
137
|
+
## Run arol in CI
|
|
138
|
+
|
|
139
|
+
Add a workflow file at `.github/workflows/arol.yml` in **your own repo**. GitHub Actions runs anything in `.github/workflows/` automatically — the filename is arbitrary — and this one triggers on every `push` and `pull_request`:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
name: arol deprecation scan
|
|
143
|
+
on: [push, pull_request]
|
|
144
|
+
jobs:
|
|
145
|
+
scan:
|
|
146
|
+
runs-on: ubuntu-latest
|
|
147
|
+
steps:
|
|
148
|
+
- uses: actions/checkout@v4
|
|
149
|
+
- uses: actions/setup-node@v4
|
|
150
|
+
with: { node-version: 20 }
|
|
151
|
+
- run: npx arol-ai scan
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
1. **Exit codes fail the build only on real breaks.** The scan exits non-zero only for high-severity or imminent dated deprecations, and stays warn-only (exit `0`) for `deprecated`/`medium` findings — so it won't block a PR over a deprecation that has no deadline.
|
|
155
|
+
2. **Portable to any CI.** `npx arol-ai scan` is the one line that matters (GitLab CI, CircleCI, a cron job, etc.); only the YAML wrapper above is GitHub-specific.
|
|
156
|
+
|
|
134
157
|
## The dataset (`deprecations.json`)
|
|
135
158
|
|
|
136
159
|
All detections are **data-driven** — the bundled dataset lives at
|
|
@@ -148,7 +171,7 @@ The dataset is either a bare array of entries, or a `{ "deprecations": [ ... ] }
|
|
|
148
171
|
"severity": "high", // "high" | "medium" | "low" (required)
|
|
149
172
|
"match": "pattern", // "pattern" (default) | "sdk" | "version"
|
|
150
173
|
"applies_to": ["py","js","ts","jsx","tsx","mjs"], // extensions to test; ["*"] = any
|
|
151
|
-
"sunset_date": "2026-08-26", // ISO YYYY-MM-DD, or
|
|
174
|
+
"sunset_date": "2026-08-26", // ISO YYYY-MM-DD, or null if no date announced
|
|
152
175
|
"detect": {
|
|
153
176
|
"sdk": ["openai"], // scope hint for "pattern"; the trigger for "sdk"/"version"
|
|
154
177
|
"patterns": [ // raw regexes: identifiers, endpoints, params
|
|
@@ -211,13 +234,31 @@ A `version` entry instead flags on the installed SDK version (no patterns needed
|
|
|
211
234
|
"severity": "medium",
|
|
212
235
|
"match": "version",
|
|
213
236
|
"version_range": "<3.0.0", // flags only when the declared version is in range
|
|
214
|
-
"sunset_date":
|
|
237
|
+
"sunset_date": null,
|
|
215
238
|
"detect": { "sdk": ["example-sdk"], "patterns": [] },
|
|
216
239
|
"migration_url": "https://example.com/migrate",
|
|
217
240
|
"summary": "Upgrade example-sdk to v3+."
|
|
218
241
|
}
|
|
219
242
|
```
|
|
220
243
|
|
|
244
|
+
A **dateless** entry — deprecated with no removal date announced. `sunset_date: null`
|
|
245
|
+
makes its status `deprecated`; it renders *"deprecated · no removal date announced"* and
|
|
246
|
+
is warn-only (exit 0) unless it's high-severity:
|
|
247
|
+
|
|
248
|
+
```jsonc
|
|
249
|
+
{
|
|
250
|
+
"id": "resend-audiences-deprecated",
|
|
251
|
+
"vendor": "Resend",
|
|
252
|
+
"title": "Audiences API (deprecated in favor of Segments)",
|
|
253
|
+
"severity": "medium",
|
|
254
|
+
"match": "pattern",
|
|
255
|
+
"sunset_date": null, // no removal date → status derives to "deprecated"
|
|
256
|
+
"detect": { "sdk": ["resend"], "patterns": ["\\.audiences\\."] },
|
|
257
|
+
"migration_url": "https://resend.com/docs/dashboard/segments",
|
|
258
|
+
"summary": "Migrate audiences.* calls to the Segments API."
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
221
262
|
### Field reference
|
|
222
263
|
|
|
223
264
|
| Field | Type | Required | Notes |
|
|
@@ -225,14 +266,15 @@ A `version` entry instead flags on the installed SDK version (no patterns needed
|
|
|
225
266
|
| `id` | string | ✓ | Unique, stable slug. |
|
|
226
267
|
| `vendor` | string | ✓ | Displayed before the title. |
|
|
227
268
|
| `title` | string | ✓ | Short headline for the finding. |
|
|
228
|
-
| `severity` | `"high"` \| `"medium"` \| `"low"` | ✓ | Drives color, sort order, and
|
|
269
|
+
| `severity` | `"high"` \| `"medium"` \| `"low"` | ✓ | Drives color, sort order, and the CI gate (high always fails). |
|
|
229
270
|
| `match` | `"pattern"` \| `"sdk"` \| `"version"` | – | How the entry is triggered. **Defaults to `"pattern"`** when omitted. See [How detection works](#how-detection-works). |
|
|
271
|
+
| `status` | `"deprecated"` \| `"scheduled"` \| `"retired"` | – | Lifecycle status. **Usually omit** — it's derived at runtime from `sunset_date`. Set explicitly only to override (e.g. force `"deprecated"`). |
|
|
230
272
|
| `applies_to` | string[] | – | File extensions (no dot) the entry's patterns/models are tested against, e.g. `["py"]` or `["js","ts","jsx","tsx","mjs"]`. Use `["*"]` for any file (model strings). **Defaults to `["*"]`** when omitted. |
|
|
231
273
|
| `version_range` | string | – | For `match: "version"` only — e.g. `"<3.0.0"`, `">=1.2.0"`, `"=2.1.0"`. If omitted, a `version` entry behaves like `"sdk"`. |
|
|
232
|
-
| `sunset_date` | string | – | ISO `YYYY-MM-DD
|
|
274
|
+
| `sunset_date` | string \| null | – | ISO `YYYY-MM-DD`, or **`null`** when no removal date is announced. Derived status: `null` → `deprecated`, future → `scheduled` (`"sunsets {date} (in N days)"`), past → `retired` (`"retired {date} (N days ago)"`). A dateless entry renders *"deprecated · no removal date announced"* and never runs date math. |
|
|
233
275
|
| `detect.sdk` | string[] | – | Manifest dependency/module names. For `match: "pattern"` this is only a **scope hint and never triggers** a finding; for `sdk`/`version` it is the trigger. |
|
|
234
276
|
| `detect.patterns` | string[] | – | **JSON-escaped** regex strings (so `\d` becomes `\\d`). For code identifiers, endpoints, and params. Matched anywhere in a source file; invalid regexes are skipped safely. |
|
|
235
|
-
| `detect.models` | string[] | – | Model family names matched **only inside string literals** (quote-anchored
|
|
277
|
+
| `detect.models` | string[] | – | Model family names matched **only inside string literals** (quote-anchored; allows an optional trailing ISO date snapshot `-YYYY-MM-DD`, nothing else). Use this for model ids so prose/JSX mentions don't false-positive and a family never matches a different model. Write the raw name (e.g. `gpt-4.5-preview`) — escaping is automatic. List dated/revisioned snapshots explicitly if they use a non-ISO suffix (e.g. Anthropic's `-20241022`). |
|
|
236
278
|
| `migration_url` | string | – | Link shown in the report. |
|
|
237
279
|
| `summary` | string | – | One or two sentences of guidance. |
|
|
238
280
|
|
|
@@ -241,7 +283,7 @@ A `version` entry instead flags on the installed SDK version (no patterns needed
|
|
|
241
283
|
### Writing good patterns & models
|
|
242
284
|
|
|
243
285
|
- **Put model ids in `detect.models`, not `detect.patterns`.** A bare model id as a raw pattern matches prose, JSX, comments, and changelogs. `detect.models` requires a quoted string literal, which is what real usage looks like (`model: "o4-mini"`).
|
|
244
|
-
- For `detect.models`, write the **raw family name** (e.g. `gpt-4.5-preview`, `claude-opus-4-20250514`) — escaping and quote-anchoring are automatic.
|
|
286
|
+
- For `detect.models`, write the **raw family name** (e.g. `gpt-4.5-preview`, `claude-opus-4-20250514`) — escaping and quote-anchoring are automatic. Matching is exact except for an optional trailing ISO date snapshot, so `gpt-4o` catches `"gpt-4o"` and `"gpt-4o-2024-05-13"` but **not** `"gpt-4o-mini"`. If a deprecated snapshot uses a non-ISO suffix (Anthropic's `claude-3-5-sonnet-20241022`, Gemini's `gemini-1.5-pro-002`), add that full id as its own `models` entry.
|
|
245
287
|
- For `detect.patterns`, match the **deprecated surface itself** — the method/property (`beta\.assistants`), endpoint path (`/v1/threads`), or param (`hapikey\s*=`) — not the import or package name. Importing an SDK isn't usage; calling the removed API is. Keep them specific (`client\.chat` is too broad — it hits unrelated SDKs).
|
|
246
288
|
- Patterns are matched **case-sensitively** with the global flag over each file's contents; the file path, line number, and matched text are reported.
|
|
247
289
|
- Escape backslashes (and literal dots) for JSON: a regex `beta\.assistants` is written `"beta\\.assistants"`. (Model entries don't need this — write `gpt-4.5-preview` as-is.)
|
package/dist/cli.js
CHANGED
|
@@ -40,6 +40,7 @@ const commander_1 = require("commander");
|
|
|
40
40
|
const data_1 = require("./data");
|
|
41
41
|
const scanner_1 = require("./scanner");
|
|
42
42
|
const report_1 = require("./report");
|
|
43
|
+
const status_1 = require("./status");
|
|
43
44
|
/** Read this package's version without importing across the rootDir boundary. */
|
|
44
45
|
function readVersion() {
|
|
45
46
|
try {
|
|
@@ -61,7 +62,8 @@ function shouldUseColor(colorFlag) {
|
|
|
61
62
|
return true;
|
|
62
63
|
return Boolean(process.stdout.isTTY);
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
/** Default window (days) within which a scheduled finding fails the CI gate. */
|
|
66
|
+
const DEFAULT_WITHIN_DAYS = 30;
|
|
65
67
|
/** Commander collector so --ignore can be passed multiple times. */
|
|
66
68
|
function collectIgnore(value, previous) {
|
|
67
69
|
return previous.concat([value]);
|
|
@@ -96,6 +98,8 @@ function runScan(targetPath, opts) {
|
|
|
96
98
|
ignore: opts.ignore,
|
|
97
99
|
dataPath: opts.data,
|
|
98
100
|
});
|
|
101
|
+
// One clock for the whole run, so rendering and the exit gate agree.
|
|
102
|
+
const now = new Date();
|
|
99
103
|
if (opts.json) {
|
|
100
104
|
const counts = { high: 0, medium: 0, low: 0 };
|
|
101
105
|
for (const f of result.findings)
|
|
@@ -111,6 +115,7 @@ function runScan(targetPath, opts) {
|
|
|
111
115
|
title: f.deprecation.title,
|
|
112
116
|
severity: f.deprecation.severity,
|
|
113
117
|
match: f.deprecation.match,
|
|
118
|
+
status: (0, status_1.effectiveStatus)(f.deprecation, now),
|
|
114
119
|
sunset_date: f.deprecation.sunset_date,
|
|
115
120
|
migration_url: f.deprecation.migration_url,
|
|
116
121
|
summary: f.deprecation.summary,
|
|
@@ -121,19 +126,29 @@ function runScan(targetPath, opts) {
|
|
|
121
126
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
122
127
|
}
|
|
123
128
|
else {
|
|
124
|
-
const report = (0, report_1.renderReport)(result, {
|
|
129
|
+
const report = (0, report_1.renderReport)(result, {
|
|
130
|
+
color: shouldUseColor(opts.color),
|
|
131
|
+
now,
|
|
132
|
+
});
|
|
125
133
|
process.stdout.write(report + "\n");
|
|
126
134
|
}
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// CI gate: exit non-zero only for an actionable finding — any "high"-severity
|
|
136
|
+
// finding, or a "scheduled" finding landing within `--within` days (default 30).
|
|
137
|
+
// Dateless "deprecated" and non-imminent medium/low findings are warn-only.
|
|
138
|
+
const parsedWithin = opts.within !== undefined ? parseInt(opts.within, 10) : NaN;
|
|
139
|
+
const within = Number.isFinite(parsedWithin) && parsedWithin >= 0
|
|
140
|
+
? parsedWithin
|
|
141
|
+
: DEFAULT_WITHIN_DAYS;
|
|
142
|
+
const tripped = result.findings.some((f) => {
|
|
143
|
+
if (f.deprecation.severity === "high")
|
|
144
|
+
return true;
|
|
145
|
+
if ((0, status_1.effectiveStatus)(f.deprecation, now) !== "scheduled")
|
|
146
|
+
return false;
|
|
147
|
+
const days = (0, status_1.daysUntil)(f.deprecation.sunset_date, now);
|
|
148
|
+
return days !== null && days >= 0 && days <= within;
|
|
149
|
+
});
|
|
150
|
+
if (tripped)
|
|
151
|
+
process.exitCode = 1;
|
|
137
152
|
}
|
|
138
153
|
function main(argv) {
|
|
139
154
|
const program = new commander_1.Command();
|
|
@@ -150,7 +165,7 @@ function main(argv) {
|
|
|
150
165
|
.option("--no-color", "disable colored output")
|
|
151
166
|
.option("--data <file>", "use a custom deprecations.json dataset instead of the bundled one")
|
|
152
167
|
.option("--ignore <glob>", "skip files matching this glob (repeatable); also reads .arolignore", collectIgnore, [])
|
|
153
|
-
.option("--
|
|
168
|
+
.option("--within <days>", "fail (exit 1) on scheduled sunsets landing within this many days (default 30); high-severity findings always fail")
|
|
154
169
|
.action((pathArg, options) => {
|
|
155
170
|
runScan(pathArg, options);
|
|
156
171
|
});
|
package/dist/data.js
CHANGED
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const SEVERITIES = ["high", "medium", "low"];
|
|
40
40
|
const MATCH_MODES = ["pattern", "sdk", "version"];
|
|
41
|
+
const STATUSES = ["deprecated", "scheduled", "retired"];
|
|
41
42
|
/**
|
|
42
43
|
* Locate the bundled deprecations.json. Tries several candidate locations so the
|
|
43
44
|
* tool works both when running the compiled output (dist/) from a published
|
|
@@ -89,6 +90,12 @@ function coerceDeprecation(raw) {
|
|
|
89
90
|
const version_range = isNonEmptyString(r.version_range)
|
|
90
91
|
? r.version_range
|
|
91
92
|
: undefined;
|
|
93
|
+
// Dateless deprecations: null / missing / "" all mean "no removal date".
|
|
94
|
+
const sunset_date = isNonEmptyString(r.sunset_date) ? r.sunset_date : null;
|
|
95
|
+
// Honor an explicit, valid status; otherwise leave it for runtime derivation.
|
|
96
|
+
const status = STATUSES.includes(r.status)
|
|
97
|
+
? r.status
|
|
98
|
+
: undefined;
|
|
92
99
|
// Language scoping: extensions this entry's patterns are valid in.
|
|
93
100
|
// Normalize to lowercase, dot-stripped; default to ["*"] (match any file).
|
|
94
101
|
const applies_to = isStringArray(r.applies_to) && r.applies_to.length > 0
|
|
@@ -106,8 +113,9 @@ function coerceDeprecation(raw) {
|
|
|
106
113
|
title: r.title,
|
|
107
114
|
severity: r.severity,
|
|
108
115
|
match,
|
|
116
|
+
status,
|
|
109
117
|
applies_to,
|
|
110
|
-
sunset_date
|
|
118
|
+
sunset_date,
|
|
111
119
|
detect: { sdk, patterns, models },
|
|
112
120
|
version_range,
|
|
113
121
|
migration_url: typeof r.migration_url === "string" ? r.migration_url : "",
|
package/dist/report.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderReport = renderReport;
|
|
4
|
+
const status_1 = require("./status");
|
|
4
5
|
function makeStyler(enabled) {
|
|
5
6
|
const wrap = (open, close) => (s) => enabled ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
6
7
|
return {
|
|
@@ -40,32 +41,28 @@ function severityPill(s, sev) {
|
|
|
40
41
|
return s.bgYellow(s.black(s.bold(label)));
|
|
41
42
|
return s.bgBlue(s.white(s.bold(label)));
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
function dayCount(n) {
|
|
45
|
+
return `${n} ${n === 1 ? "day" : "days"}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render the lifecycle line for a finding, keyed on its (effective) status.
|
|
49
|
+
* Date math runs ONLY for dated statuses, so a null sunset_date never produces NaN.
|
|
50
|
+
*/
|
|
51
|
+
function statusPhrase(s, d, now) {
|
|
52
|
+
const status = (0, status_1.effectiveStatus)(d, now);
|
|
53
|
+
const days = (0, status_1.daysUntil)(d.sunset_date, now);
|
|
54
|
+
// Dateless (or, defensively, a dated status with no parseable date).
|
|
55
|
+
if (status === "deprecated" || days === null) {
|
|
56
|
+
return s.yellow("deprecated · no removal date announced — migrate before it's pulled");
|
|
47
57
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return s.
|
|
58
|
+
if (status === "retired") {
|
|
59
|
+
const ago = Math.abs(days);
|
|
60
|
+
return s.red(`retired ${d.sunset_date} (${dayCount(ago)} ago)`);
|
|
51
61
|
}
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rel = `in ${days} days`;
|
|
57
|
-
else if (days === 1)
|
|
58
|
-
rel = "in 1 day";
|
|
59
|
-
else if (days === 0)
|
|
60
|
-
rel = "today";
|
|
61
|
-
else if (days === -1)
|
|
62
|
-
rel = "1 day ago";
|
|
63
|
-
else
|
|
64
|
-
rel = `${Math.abs(days)} days ago`;
|
|
65
|
-
const base = `sunsets ${sunsetDate}`;
|
|
66
|
-
const hint = days <= 0 ? ` (passed ${rel})` : ` (${rel})`;
|
|
67
|
-
// Past or imminent sunsets are the urgent ones.
|
|
68
|
-
return days <= 30 ? s.red(base + hint) : s.yellow(base + hint);
|
|
62
|
+
// scheduled
|
|
63
|
+
const rel = days <= 0 ? "today" : `in ${dayCount(days)}`;
|
|
64
|
+
const line = `sunsets ${d.sunset_date} (${rel})`;
|
|
65
|
+
return days <= 30 ? s.red(line) : s.yellow(line);
|
|
69
66
|
}
|
|
70
67
|
/** One line per source location: "path:line → matched text". */
|
|
71
68
|
function formatPatternMatches(s, finding) {
|
|
@@ -98,9 +95,10 @@ function renderReport(result, opts) {
|
|
|
98
95
|
SEVERITY_ORDER[b.deprecation.severity];
|
|
99
96
|
if (sevDiff !== 0)
|
|
100
97
|
return sevDiff;
|
|
101
|
-
// Within a severity, soonest
|
|
102
|
-
|
|
103
|
-
const
|
|
98
|
+
// Within a severity, soonest sunset first; dateless ("deprecated") entries
|
|
99
|
+
// sort last. A null/empty date maps to a far-future sentinel (no throwing).
|
|
100
|
+
const da = a.deprecation.sunset_date || "9999-12-31";
|
|
101
|
+
const db = b.deprecation.sunset_date || "9999-12-31";
|
|
104
102
|
return da.localeCompare(db);
|
|
105
103
|
});
|
|
106
104
|
// Header.
|
|
@@ -114,7 +112,7 @@ function renderReport(result, opts) {
|
|
|
114
112
|
if (findings.length === 0) {
|
|
115
113
|
out.push(s.green(s.bold("✓ No upcoming deprecations detected in your stack.")));
|
|
116
114
|
out.push("");
|
|
117
|
-
out.push(footer(s, findings));
|
|
115
|
+
out.push(footer(s, findings, now));
|
|
118
116
|
return out.join("\n");
|
|
119
117
|
}
|
|
120
118
|
// Severity summary.
|
|
@@ -136,7 +134,7 @@ function renderReport(result, opts) {
|
|
|
136
134
|
const d = finding.deprecation;
|
|
137
135
|
const sevColor = severityColor(s, d.severity);
|
|
138
136
|
out.push(`${sevColor("●")} ${s.bold(d.vendor)} ${s.gray("·")} ${d.title} ${severityPill(s, d.severity)}`);
|
|
139
|
-
out.push(` ${
|
|
137
|
+
out.push(` ${statusPhrase(s, d, now)}`);
|
|
140
138
|
if (d.summary) {
|
|
141
139
|
out.push(` ${s.dim(wrapText(d.summary, 76, " ").trimStart())}`);
|
|
142
140
|
}
|
|
@@ -156,28 +154,26 @@ function renderReport(result, opts) {
|
|
|
156
154
|
}
|
|
157
155
|
out.push("");
|
|
158
156
|
}
|
|
159
|
-
out.push(footer(s, findings));
|
|
157
|
+
out.push(footer(s, findings, now));
|
|
160
158
|
return out.join("\n");
|
|
161
159
|
}
|
|
162
|
-
/**
|
|
163
|
-
function footer(s, findings) {
|
|
160
|
+
/** Status-aware closing CTA, visually separated from the findings above. */
|
|
161
|
+
function footer(s, findings, now) {
|
|
164
162
|
const sep = s.dim("─".repeat(60));
|
|
165
163
|
const brand = "arol.ai";
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
else if (findings.length > 0) {
|
|
172
|
-
message =
|
|
173
|
-
s.dim("Get continuous deprecation alerts for your stack → ") +
|
|
174
|
-
s.cyan(s.bold(brand));
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
message =
|
|
178
|
-
s.green("✓ Clean today — but new deprecations land constantly. Stay covered → ") +
|
|
179
|
-
s.cyan(s.bold(brand));
|
|
164
|
+
if (findings.length === 0) {
|
|
165
|
+
const message = s.green("✓ Clean today — but new deprecations land constantly. Stay covered → ") +
|
|
166
|
+
s.cyan(s.bold(brand));
|
|
167
|
+
return [sep, message].join("\n");
|
|
180
168
|
}
|
|
169
|
+
// The urgent line only makes sense when something actually breaks on a date:
|
|
170
|
+
// a high-severity finding, or any dated (scheduled/retired) finding.
|
|
171
|
+
const hasHighOrDated = findings.some((f) => f.deprecation.severity === "high" ||
|
|
172
|
+
(0, status_1.effectiveStatus)(f.deprecation, now) !== "deprecated");
|
|
173
|
+
const message = hasHighOrDated
|
|
174
|
+
? s.bold(s.red(`⚠ These break on fixed dates. Get alerted before the next one hits you → ${brand}`))
|
|
175
|
+
: s.yellow("Deprecated APIs in your stack will be pulled eventually — stay ahead → ") +
|
|
176
|
+
s.cyan(s.bold(brand));
|
|
181
177
|
return [sep, message].join("\n");
|
|
182
178
|
}
|
|
183
179
|
/** Soft-wrap text to a width, indenting continuation lines. */
|
package/dist/scanner.js
CHANGED
|
@@ -91,13 +91,14 @@ function escapeRegex(s) {
|
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
93
|
* Build a regex that matches a model family ONLY inside a string literal:
|
|
94
|
-
* an opening quote, the (escaped) family name, an
|
|
95
|
-
* the SAME closing quote.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
94
|
+
* an opening quote, the (escaped) family name, an OPTIONAL ISO date snapshot
|
|
95
|
+
* suffix (e.g. -2024-05-13), then the SAME closing quote. No arbitrary trailing
|
|
96
|
+
* characters are allowed, so "gpt-4o" matches "gpt-4o" and "gpt-4o-2024-05-13"
|
|
97
|
+
* but never a different model like "gpt-4o-mini" or "gpt-4o-realtime-preview".
|
|
98
|
+
* The model is still only found inside a quoted literal, never in bare prose.
|
|
98
99
|
*/
|
|
99
100
|
function modelRegexSource(family) {
|
|
100
|
-
return `(${QUOTE_CLASS})${escapeRegex(family)}
|
|
101
|
+
return `(${QUOTE_CLASS})${escapeRegex(family)}(?:-\\d{4}-\\d{2}-\\d{2})?\\1`;
|
|
101
102
|
}
|
|
102
103
|
function compileDeprecations(deprecations) {
|
|
103
104
|
return deprecations.map((deprecation) => {
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSunsetDate = parseSunsetDate;
|
|
4
|
+
exports.daysUntil = daysUntil;
|
|
5
|
+
exports.effectiveStatus = effectiveStatus;
|
|
6
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
7
|
+
/** Midnight-UTC timestamp for a Date's calendar day. */
|
|
8
|
+
function startOfDayUTC(d) {
|
|
9
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse an ISO `YYYY-MM-DD` sunset date to a UTC midnight timestamp.
|
|
13
|
+
* Returns null for null/empty/unparseable input — callers must treat null as
|
|
14
|
+
* "no usable date" and skip all date math.
|
|
15
|
+
*/
|
|
16
|
+
function parseSunsetDate(date) {
|
|
17
|
+
if (!date)
|
|
18
|
+
return null;
|
|
19
|
+
const t = new Date(`${date}T00:00:00Z`).getTime();
|
|
20
|
+
return Number.isNaN(t) ? null : t;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whole calendar days from today to the sunset date: positive = in the future,
|
|
24
|
+
* negative = in the past, 0 = today. Returns null when there is no usable date,
|
|
25
|
+
* so the result can never be NaN.
|
|
26
|
+
*/
|
|
27
|
+
function daysUntil(date, now) {
|
|
28
|
+
const t = parseSunsetDate(date);
|
|
29
|
+
if (t === null)
|
|
30
|
+
return null;
|
|
31
|
+
return Math.round((t - startOfDayUTC(now)) / MS_PER_DAY);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The effective status for a finding. An explicit `status` is always honored;
|
|
35
|
+
* otherwise it is derived from sunset_date relative to `now`:
|
|
36
|
+
* null/absent → "deprecated", past → "retired", today or future → "scheduled".
|
|
37
|
+
*/
|
|
38
|
+
function effectiveStatus(d, now) {
|
|
39
|
+
if (d.status)
|
|
40
|
+
return d.status;
|
|
41
|
+
const t = parseSunsetDate(d.sunset_date);
|
|
42
|
+
if (t === null)
|
|
43
|
+
return "deprecated";
|
|
44
|
+
return t < startOfDayUTC(now) ? "retired" : "scheduled";
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -128,32 +128,20 @@
|
|
|
128
128
|
"source": "https://firebase.google.com/docs/ai-logic/models"
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
|
-
"id": "openai-gpt4-
|
|
131
|
+
"id": "openai-retired-gpt4-variants",
|
|
132
132
|
"vendor": "OpenAI",
|
|
133
|
-
"title": "GPT-4
|
|
133
|
+
"title": "Retired GPT-4 preview/variant models",
|
|
134
134
|
"severity": "high",
|
|
135
135
|
"match": "pattern",
|
|
136
136
|
"applies_to": ["*"],
|
|
137
|
-
"sunset_date": "
|
|
138
|
-
"date_confidence": "verify",
|
|
137
|
+
"sunset_date": "2025-07-14",
|
|
139
138
|
"detect": {
|
|
140
139
|
"sdk": ["openai"],
|
|
141
|
-
"
|
|
142
|
-
"models": [
|
|
143
|
-
"gpt-4o",
|
|
144
|
-
"gpt-4-turbo",
|
|
145
|
-
"gpt-4-0613",
|
|
146
|
-
"gpt-4-0125-preview",
|
|
147
|
-
"gpt-4-1106-preview",
|
|
148
|
-
"gpt-4-32k",
|
|
149
|
-
"gpt-4-vision-preview",
|
|
150
|
-
"o4-mini",
|
|
151
|
-
"gpt-4.5-preview"
|
|
152
|
-
]
|
|
140
|
+
"models": ["gpt-4-vision-preview", "gpt-4-32k", "gpt-4.5-preview"]
|
|
153
141
|
},
|
|
154
142
|
"migration_url": "https://platform.openai.com/docs/deprecations",
|
|
155
|
-
"summary": "
|
|
156
|
-
"source": "https://
|
|
143
|
+
"summary": "These GPT-4 variant/preview models are already retired from the API and return errors: gpt-4-vision-preview (~Dec 2024), gpt-4-32k (~mid-2025), gpt-4.5-preview (July 14, 2025). Migrate to gpt-4.1 or the GPT-5 family.",
|
|
144
|
+
"source": "https://developers.openai.com/api/docs/deprecations"
|
|
157
145
|
},
|
|
158
146
|
{
|
|
159
147
|
"id": "openai-legacy-retired-models",
|
|
@@ -350,5 +338,22 @@
|
|
|
350
338
|
"migration_url": "https://python.langchain.com/docs/versions/v0_2/",
|
|
351
339
|
"summary": "Pre-0.2 LangChain imports are deprecated/removed: langchain.llms/chat_models/embeddings → langchain_community or provider packages (langchain_openai). LLMChain and initialize_agent are deprecated (use LCEL / create_agent; legacy moved to langchain-classic in 1.0).",
|
|
352
340
|
"source": "https://github.com/langchain-ai/langchain/discussions/19083"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"id": "resend-audiences-deprecated",
|
|
344
|
+
"vendor": "Resend",
|
|
345
|
+
"title": "Audiences API (deprecated in favor of Segments)",
|
|
346
|
+
"severity": "medium",
|
|
347
|
+
"match": "pattern",
|
|
348
|
+
"applies_to": ["js", "ts", "jsx", "tsx", "mjs", "py"],
|
|
349
|
+
"status": "deprecated",
|
|
350
|
+
"sunset_date": null,
|
|
351
|
+
"detect": {
|
|
352
|
+
"sdk": ["resend"],
|
|
353
|
+
"patterns": ["\\.audiences\\.", "\\.Audiences\\."]
|
|
354
|
+
},
|
|
355
|
+
"migration_url": "https://resend.com/docs/dashboard/segments/migrating-from-audiences-to-segments",
|
|
356
|
+
"summary": "Resend's Audiences API is deprecated in favor of Segments. The endpoints still work but will be removed in the future (no date announced). Migrate the audiences.* calls to the Segments API.",
|
|
357
|
+
"source": "https://resend.com/docs/api-reference/audiences/create-audience"
|
|
353
358
|
}
|
|
354
359
|
]
|