arol-ai 0.1.2 → 0.1.4
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 +59 -12
- package/dist/cli.js +28 -13
- package/dist/data.js +15 -1
- package/dist/report.js +42 -46
- package/dist/scanner.js +118 -7
- package/dist/status.js +45 -0
- package/package.json +1 -1
- package/src/data/deprecations.json +108 -19
package/README.md
CHANGED
|
@@ -72,13 +72,18 @@ 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
|
|
|
79
|
+
Two more layers keep matches context-aware:
|
|
80
|
+
|
|
81
|
+
- **Language scoping (`applies_to`).** Each entry lists the file extensions its signals are valid in (e.g. `["py"]`, `["js","ts","jsx","tsx","mjs"]`, or `["*"]` for model strings). An entry is only tested against files with a matching extension, so a Python-only pattern like `openai.ChatCompletion` never fires in a `.tsx` file. Defaults to `["*"]` when omitted.
|
|
82
|
+
- **Comment stripping.** Before matching, comments are blanked out per language — `//`, `/* */`, JSX `{/* */}`, and `#` (Python). Stripping is string-aware: a marker inside a string literal (e.g. the `//` in `"https://…"`) is **not** treated as a comment, and offsets are preserved so reported line numbers stay exact. A commented-out `model: "gpt-4o"` is ignored; the real call on the next line is not.
|
|
83
|
+
|
|
79
84
|
Each hit records the **file path, line number, and matched text**, and one deprecation aggregates **all** of its matched locations into a single finding.
|
|
80
85
|
|
|
81
|
-
> Having the `openai` package in `requirements.txt` does **not** flag the Assistants API deprecation. Your code has to actually use `beta.assistants` / a deprecated model id (etc.).
|
|
86
|
+
> Having the `openai` package in `requirements.txt` does **not** flag the Assistants API deprecation. Your code has to actually use `beta.assistants` / a deprecated model id (etc.), in a file of the right language, outside comments.
|
|
82
87
|
|
|
83
88
|
**Files scanned / skipped**
|
|
84
89
|
|
|
@@ -112,16 +117,19 @@ arol-ai scan [path] [options]
|
|
|
112
117
|
| `--no-color` | Disable colored output (also respects `NO_COLOR`) |
|
|
113
118
|
| `--data <file>` | Use a custom `deprecations.json` instead of the bundled one |
|
|
114
119
|
| `--ignore <glob>` | Skip files matching this glob; repeatable. Combined with `.arolignore`. e.g. `--ignore 'docs/**' --ignore '**/*.gen.ts'` |
|
|
115
|
-
| `--
|
|
120
|
+
| `--within <days>` | Window (default `30`) for the CI gate's "scheduled soon" check. See exit codes. |
|
|
116
121
|
| `-v, --version` | Print the version |
|
|
117
122
|
| `-h, --help` | Show help |
|
|
118
123
|
|
|
119
124
|
`scan` is the default command, so `arol-ai ./repo` works too.
|
|
120
125
|
|
|
121
|
-
**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:
|
|
122
129
|
|
|
123
130
|
```sh
|
|
124
|
-
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
|
|
125
133
|
```
|
|
126
134
|
|
|
127
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.
|
|
@@ -142,7 +150,8 @@ The dataset is either a bare array of entries, or a `{ "deprecations": [ ... ] }
|
|
|
142
150
|
"title": "Assistants API (beta)", // short headline (required)
|
|
143
151
|
"severity": "high", // "high" | "medium" | "low" (required)
|
|
144
152
|
"match": "pattern", // "pattern" (default) | "sdk" | "version"
|
|
145
|
-
"
|
|
153
|
+
"applies_to": ["py","js","ts","jsx","tsx","mjs"], // extensions to test; ["*"] = any
|
|
154
|
+
"sunset_date": "2026-08-26", // ISO YYYY-MM-DD, or null if no date announced
|
|
146
155
|
"detect": {
|
|
147
156
|
"sdk": ["openai"], // scope hint for "pattern"; the trigger for "sdk"/"version"
|
|
148
157
|
"patterns": [ // raw regexes: identifiers, endpoints, params
|
|
@@ -157,7 +166,7 @@ The dataset is either a bare array of entries, or a `{ "deprecations": [ ... ] }
|
|
|
157
166
|
}
|
|
158
167
|
```
|
|
159
168
|
|
|
160
|
-
A model-retirement entry uses `detect.models` so it only fires on a quoted model id, never on prose:
|
|
169
|
+
A model-retirement entry uses `detect.models` (and `applies_to: ["*"]`) so it only fires on a quoted model id, in any language, never on prose:
|
|
161
170
|
|
|
162
171
|
```jsonc
|
|
163
172
|
{
|
|
@@ -166,6 +175,7 @@ A model-retirement entry uses `detect.models` so it only fires on a quoted model
|
|
|
166
175
|
"title": "GPT-4 family models (API shutdown)",
|
|
167
176
|
"severity": "high",
|
|
168
177
|
"match": "pattern",
|
|
178
|
+
"applies_to": ["*"],
|
|
169
179
|
"sunset_date": "2026-10-23",
|
|
170
180
|
"detect": {
|
|
171
181
|
"sdk": ["openai"],
|
|
@@ -177,6 +187,23 @@ A model-retirement entry uses `detect.models` so it only fires on a quoted model
|
|
|
177
187
|
}
|
|
178
188
|
```
|
|
179
189
|
|
|
190
|
+
A Python-only entry scopes itself with `applies_to: ["py"]`, so its patterns never fire in JS/TSX files that merely mention the API in prose:
|
|
191
|
+
|
|
192
|
+
```jsonc
|
|
193
|
+
{
|
|
194
|
+
"id": "openai-python-v0-syntax",
|
|
195
|
+
"vendor": "OpenAI",
|
|
196
|
+
"title": "Legacy openai-python v0 call syntax",
|
|
197
|
+
"severity": "high",
|
|
198
|
+
"match": "pattern",
|
|
199
|
+
"applies_to": ["py"],
|
|
200
|
+
"sunset_date": "2023-11-06",
|
|
201
|
+
"detect": { "sdk": ["openai"], "patterns": ["openai\\.ChatCompletion"] },
|
|
202
|
+
"migration_url": "https://github.com/openai/openai-python/discussions/742",
|
|
203
|
+
"summary": "Instantiate a client: client.chat.completions.create(...)."
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
180
207
|
A `version` entry instead flags on the installed SDK version (no patterns needed):
|
|
181
208
|
|
|
182
209
|
```jsonc
|
|
@@ -187,13 +214,31 @@ A `version` entry instead flags on the installed SDK version (no patterns needed
|
|
|
187
214
|
"severity": "medium",
|
|
188
215
|
"match": "version",
|
|
189
216
|
"version_range": "<3.0.0", // flags only when the declared version is in range
|
|
190
|
-
"sunset_date":
|
|
217
|
+
"sunset_date": null,
|
|
191
218
|
"detect": { "sdk": ["example-sdk"], "patterns": [] },
|
|
192
219
|
"migration_url": "https://example.com/migrate",
|
|
193
220
|
"summary": "Upgrade example-sdk to v3+."
|
|
194
221
|
}
|
|
195
222
|
```
|
|
196
223
|
|
|
224
|
+
A **dateless** entry — deprecated with no removal date announced. `sunset_date: null`
|
|
225
|
+
makes its status `deprecated`; it renders *"deprecated · no removal date announced"* and
|
|
226
|
+
is warn-only (exit 0) unless it's high-severity:
|
|
227
|
+
|
|
228
|
+
```jsonc
|
|
229
|
+
{
|
|
230
|
+
"id": "resend-audiences-deprecated",
|
|
231
|
+
"vendor": "Resend",
|
|
232
|
+
"title": "Audiences API (deprecated in favor of Segments)",
|
|
233
|
+
"severity": "medium",
|
|
234
|
+
"match": "pattern",
|
|
235
|
+
"sunset_date": null, // no removal date → status derives to "deprecated"
|
|
236
|
+
"detect": { "sdk": ["resend"], "patterns": ["\\.audiences\\."] },
|
|
237
|
+
"migration_url": "https://resend.com/docs/dashboard/segments",
|
|
238
|
+
"summary": "Migrate audiences.* calls to the Segments API."
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
197
242
|
### Field reference
|
|
198
243
|
|
|
199
244
|
| Field | Type | Required | Notes |
|
|
@@ -201,13 +246,15 @@ A `version` entry instead flags on the installed SDK version (no patterns needed
|
|
|
201
246
|
| `id` | string | ✓ | Unique, stable slug. |
|
|
202
247
|
| `vendor` | string | ✓ | Displayed before the title. |
|
|
203
248
|
| `title` | string | ✓ | Short headline for the finding. |
|
|
204
|
-
| `severity` | `"high"` \| `"medium"` \| `"low"` | ✓ | Drives color, sort order, and
|
|
249
|
+
| `severity` | `"high"` \| `"medium"` \| `"low"` | ✓ | Drives color, sort order, and the CI gate (high always fails). |
|
|
205
250
|
| `match` | `"pattern"` \| `"sdk"` \| `"version"` | – | How the entry is triggered. **Defaults to `"pattern"`** when omitted. See [How detection works](#how-detection-works). |
|
|
251
|
+
| `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"`). |
|
|
252
|
+
| `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. |
|
|
206
253
|
| `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"`. |
|
|
207
|
-
| `sunset_date` | string | – | ISO `YYYY-MM-DD
|
|
254
|
+
| `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. |
|
|
208
255
|
| `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. |
|
|
209
256
|
| `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. |
|
|
210
|
-
| `detect.models` | string[] | – | Model family names matched **only inside string literals** (quote-anchored
|
|
257
|
+
| `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`). |
|
|
211
258
|
| `migration_url` | string | – | Link shown in the report. |
|
|
212
259
|
| `summary` | string | – | One or two sentences of guidance. |
|
|
213
260
|
|
|
@@ -216,7 +263,7 @@ A `version` entry instead flags on the installed SDK version (no patterns needed
|
|
|
216
263
|
### Writing good patterns & models
|
|
217
264
|
|
|
218
265
|
- **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"`).
|
|
219
|
-
- For `detect.models`, write the **raw family name** (e.g. `gpt-4.5-preview`, `claude-opus-4-20250514`) — escaping and quote-anchoring are automatic.
|
|
266
|
+
- 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.
|
|
220
267
|
- 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).
|
|
221
268
|
- Patterns are matched **case-sensitively** with the global flag over each file's contents; the file path, line number, and matched text are reported.
|
|
222
269
|
- 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,17 @@ 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;
|
|
99
|
+
// Language scoping: extensions this entry's patterns are valid in.
|
|
100
|
+
// Normalize to lowercase, dot-stripped; default to ["*"] (match any file).
|
|
101
|
+
const applies_to = isStringArray(r.applies_to) && r.applies_to.length > 0
|
|
102
|
+
? r.applies_to.map((e) => e.toLowerCase().replace(/^\./, ""))
|
|
103
|
+
: ["*"];
|
|
92
104
|
// Drop entries that can never fire under their match mode.
|
|
93
105
|
// A "pattern" entry fires on either a raw pattern OR a model-string match.
|
|
94
106
|
if (match === "pattern" && patterns.length === 0 && models.length === 0)
|
|
@@ -101,7 +113,9 @@ function coerceDeprecation(raw) {
|
|
|
101
113
|
title: r.title,
|
|
102
114
|
severity: r.severity,
|
|
103
115
|
match,
|
|
104
|
-
|
|
116
|
+
status,
|
|
117
|
+
applies_to,
|
|
118
|
+
sunset_date,
|
|
105
119
|
detect: { sdk, patterns, models },
|
|
106
120
|
version_range,
|
|
107
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) => {
|
|
@@ -121,9 +122,110 @@ function compileDeprecations(deprecations) {
|
|
|
121
122
|
// Defensive: a pathological family name must not crash the scan.
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
+
const appliesTo = new Set((deprecation.applies_to.length > 0 ? deprecation.applies_to : ["*"]).map((e) => e.toLowerCase()));
|
|
126
|
+
return { deprecation, regexes, appliesTo };
|
|
125
127
|
});
|
|
126
128
|
}
|
|
129
|
+
/** True if a compiled entry should be tested against a file with this extension. */
|
|
130
|
+
function appliesToExt(compiled, ext) {
|
|
131
|
+
return compiled.appliesTo.has("*") || compiled.appliesTo.has(ext);
|
|
132
|
+
}
|
|
133
|
+
function commentConfig(ext) {
|
|
134
|
+
switch (ext) {
|
|
135
|
+
case "js":
|
|
136
|
+
case "mjs":
|
|
137
|
+
case "cjs":
|
|
138
|
+
case "jsx":
|
|
139
|
+
case "ts":
|
|
140
|
+
case "mts":
|
|
141
|
+
case "cts":
|
|
142
|
+
case "tsx":
|
|
143
|
+
case "go":
|
|
144
|
+
return { line: ["//"], block: [["/*", "*/"]], strings: ["'", '"', "`"], triple: [] };
|
|
145
|
+
case "py":
|
|
146
|
+
return { line: ["#"], block: [], strings: ["'", '"'], triple: ['"""', "'''"] };
|
|
147
|
+
default:
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Replace comments with spaces so they don't match, while preserving the exact
|
|
153
|
+
* byte length and all newlines — line/column offsets stay correct. String literals
|
|
154
|
+
* (including their contents) are left intact, so a comment marker inside a string
|
|
155
|
+
* (e.g. "https://…") is NOT treated as a comment.
|
|
156
|
+
*/
|
|
157
|
+
function stripComments(src, cfg) {
|
|
158
|
+
const out = src.split("");
|
|
159
|
+
const n = src.length;
|
|
160
|
+
const at = (s, i) => src.startsWith(s, i);
|
|
161
|
+
const blank = (from, to) => {
|
|
162
|
+
for (let k = from; k < to; k++)
|
|
163
|
+
if (out[k] !== "\n")
|
|
164
|
+
out[k] = " ";
|
|
165
|
+
};
|
|
166
|
+
let i = 0;
|
|
167
|
+
while (i < n) {
|
|
168
|
+
// Triple-quoted strings (Python) — checked before single quotes.
|
|
169
|
+
let matched = false;
|
|
170
|
+
for (const t of cfg.triple) {
|
|
171
|
+
if (at(t, i)) {
|
|
172
|
+
i += t.length;
|
|
173
|
+
while (i < n && !at(t, i))
|
|
174
|
+
i++;
|
|
175
|
+
i += t.length;
|
|
176
|
+
matched = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (matched)
|
|
181
|
+
continue;
|
|
182
|
+
// Ordinary string literals — preserve contents verbatim.
|
|
183
|
+
for (const q of cfg.strings) {
|
|
184
|
+
if (src[i] === q) {
|
|
185
|
+
i++;
|
|
186
|
+
while (i < n && src[i] !== q) {
|
|
187
|
+
if (src[i] === "\\")
|
|
188
|
+
i++; // skip escaped char
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
i++; // closing quote (or past end if unterminated)
|
|
192
|
+
matched = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (matched)
|
|
197
|
+
continue;
|
|
198
|
+
// Block comments.
|
|
199
|
+
for (const [open, close] of cfg.block) {
|
|
200
|
+
if (at(open, i)) {
|
|
201
|
+
const end = src.indexOf(close, i + open.length);
|
|
202
|
+
const stop = end === -1 ? n : end + close.length;
|
|
203
|
+
blank(i, stop);
|
|
204
|
+
i = stop;
|
|
205
|
+
matched = true;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (matched)
|
|
210
|
+
continue;
|
|
211
|
+
// Line comments.
|
|
212
|
+
for (const lc of cfg.line) {
|
|
213
|
+
if (at(lc, i)) {
|
|
214
|
+
let k = i;
|
|
215
|
+
while (k < n && src[k] !== "\n")
|
|
216
|
+
k++;
|
|
217
|
+
blank(i, k);
|
|
218
|
+
i = k;
|
|
219
|
+
matched = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (matched)
|
|
224
|
+
continue;
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
return out.join("");
|
|
228
|
+
}
|
|
127
229
|
/** Precompute the byte offset at which each line starts. */
|
|
128
230
|
function computeLineStarts(content) {
|
|
129
231
|
const starts = [0];
|
|
@@ -354,7 +456,16 @@ function scanRepo(root, deprecations, options = {}) {
|
|
|
354
456
|
continue;
|
|
355
457
|
}
|
|
356
458
|
scannedFiles++;
|
|
357
|
-
|
|
459
|
+
// Language scoping: only test entries valid for this file's extension.
|
|
460
|
+
const ext = path.extname(rel).slice(1).toLowerCase();
|
|
461
|
+
const applicable = compiled.filter((c) => appliesToExt(c, ext));
|
|
462
|
+
if (applicable.length === 0)
|
|
463
|
+
continue;
|
|
464
|
+
// Comment stripping: match against de-commented source so mentions in
|
|
465
|
+
// comments don't count. Offsets are preserved, so line numbers stay correct.
|
|
466
|
+
const cfg = commentConfig(ext);
|
|
467
|
+
const scanText = cfg ? stripComments(content, cfg) : content;
|
|
468
|
+
scanContent(scanText, rel, applicable, patternSink);
|
|
358
469
|
}
|
|
359
470
|
// 3. Build findings — one per deprecation, evaluated per its match mode.
|
|
360
471
|
const findings = [];
|
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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"title": "Assistants API (beta)",
|
|
6
6
|
"severity": "high",
|
|
7
7
|
"match": "pattern",
|
|
8
|
+
"applies_to": ["py", "js", "ts", "jsx", "tsx", "mjs"],
|
|
8
9
|
"sunset_date": "2026-08-26",
|
|
9
10
|
"detect": {
|
|
10
11
|
"sdk": ["openai"],
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"title": "Claude Sonnet 4 & Opus 4",
|
|
26
27
|
"severity": "high",
|
|
27
28
|
"match": "pattern",
|
|
29
|
+
"applies_to": ["*"],
|
|
28
30
|
"sunset_date": "2026-06-15",
|
|
29
31
|
"detect": {
|
|
30
32
|
"sdk": ["@anthropic-ai/sdk", "anthropic"],
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
"title": "Retired Claude models (Haiku 3, 3.5/3.7 Sonnet, Claude 2.x)",
|
|
47
49
|
"severity": "high",
|
|
48
50
|
"match": "pattern",
|
|
51
|
+
"applies_to": ["*"],
|
|
49
52
|
"sunset_date": "2026-04-20",
|
|
50
53
|
"detect": {
|
|
51
54
|
"sdk": ["@anthropic-ai/sdk", "anthropic"],
|
|
@@ -69,6 +72,7 @@
|
|
|
69
72
|
"title": "Gemini 2.5 Pro / Flash / Flash-Lite",
|
|
70
73
|
"severity": "high",
|
|
71
74
|
"match": "pattern",
|
|
75
|
+
"applies_to": ["*"],
|
|
72
76
|
"sunset_date": "2026-10-16",
|
|
73
77
|
"detect": {
|
|
74
78
|
"sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
|
|
@@ -85,6 +89,7 @@
|
|
|
85
89
|
"title": "Gemini 2.0 Flash family",
|
|
86
90
|
"severity": "high",
|
|
87
91
|
"match": "pattern",
|
|
92
|
+
"applies_to": ["*"],
|
|
88
93
|
"sunset_date": "2026-06-01",
|
|
89
94
|
"detect": {
|
|
90
95
|
"sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
|
|
@@ -106,42 +111,37 @@
|
|
|
106
111
|
"title": "Gemini 1.0 / 1.5 models",
|
|
107
112
|
"severity": "high",
|
|
108
113
|
"match": "pattern",
|
|
114
|
+
"applies_to": ["*"],
|
|
109
115
|
"sunset_date": "2026-04-29",
|
|
110
116
|
"detect": {
|
|
111
117
|
"sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
|
|
112
118
|
"patterns": [],
|
|
113
|
-
"models": [
|
|
119
|
+
"models": [
|
|
120
|
+
"gemini-1.5-pro",
|
|
121
|
+
"gemini-1.5-flash",
|
|
122
|
+
"gemini-1.0-pro",
|
|
123
|
+
"gemini-pro"
|
|
124
|
+
]
|
|
114
125
|
},
|
|
115
126
|
"migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
|
|
116
127
|
"summary": "All Gemini 1.0 and 1.5 models are shut down and return 404. Migrate to Gemini 2.5+ or 3.x.",
|
|
117
128
|
"source": "https://firebase.google.com/docs/ai-logic/models"
|
|
118
129
|
},
|
|
119
130
|
{
|
|
120
|
-
"id": "openai-gpt4-
|
|
131
|
+
"id": "openai-retired-gpt4-variants",
|
|
121
132
|
"vendor": "OpenAI",
|
|
122
|
-
"title": "GPT-4
|
|
133
|
+
"title": "Retired GPT-4 preview/variant models",
|
|
123
134
|
"severity": "high",
|
|
124
135
|
"match": "pattern",
|
|
125
|
-
"
|
|
126
|
-
"
|
|
136
|
+
"applies_to": ["*"],
|
|
137
|
+
"sunset_date": "2025-07-14",
|
|
127
138
|
"detect": {
|
|
128
139
|
"sdk": ["openai"],
|
|
129
|
-
"
|
|
130
|
-
"models": [
|
|
131
|
-
"gpt-4o",
|
|
132
|
-
"gpt-4-turbo",
|
|
133
|
-
"gpt-4-0613",
|
|
134
|
-
"gpt-4-0125-preview",
|
|
135
|
-
"gpt-4-1106-preview",
|
|
136
|
-
"gpt-4-32k",
|
|
137
|
-
"gpt-4-vision-preview",
|
|
138
|
-
"o4-mini",
|
|
139
|
-
"gpt-4.5-preview"
|
|
140
|
-
]
|
|
140
|
+
"models": ["gpt-4-vision-preview", "gpt-4-32k", "gpt-4.5-preview"]
|
|
141
141
|
},
|
|
142
142
|
"migration_url": "https://platform.openai.com/docs/deprecations",
|
|
143
|
-
"summary": "
|
|
144
|
-
"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"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"id": "openai-legacy-retired-models",
|
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
"title": "Retired legacy OpenAI models (GPT-3 era, early snapshots)",
|
|
150
150
|
"severity": "high",
|
|
151
151
|
"match": "pattern",
|
|
152
|
+
"applies_to": ["*"],
|
|
152
153
|
"sunset_date": "2024-01-04",
|
|
153
154
|
"detect": {
|
|
154
155
|
"sdk": ["openai"],
|
|
@@ -174,6 +175,7 @@
|
|
|
174
175
|
"title": "Removed legacy Stripe.js methods",
|
|
175
176
|
"severity": "high",
|
|
176
177
|
"match": "pattern",
|
|
178
|
+
"applies_to": ["js", "ts", "jsx", "tsx", "mjs"],
|
|
177
179
|
"sunset_date": "2026-03-25",
|
|
178
180
|
"detect": {
|
|
179
181
|
"sdk": ["@stripe/stripe-js", "stripe"],
|
|
@@ -197,6 +199,7 @@
|
|
|
197
199
|
"title": "Sources API / legacy Charges API",
|
|
198
200
|
"severity": "medium",
|
|
199
201
|
"match": "pattern",
|
|
202
|
+
"applies_to": ["js", "ts", "jsx", "tsx", "mjs"],
|
|
200
203
|
"sunset_date": "2024-05-15",
|
|
201
204
|
"detect": {
|
|
202
205
|
"sdk": ["stripe"],
|
|
@@ -212,6 +215,7 @@
|
|
|
212
215
|
"title": "Notify API",
|
|
213
216
|
"severity": "high",
|
|
214
217
|
"match": "pattern",
|
|
218
|
+
"applies_to": ["py", "js", "ts", "jsx", "tsx", "mjs"],
|
|
215
219
|
"sunset_date": "2027-01-31",
|
|
216
220
|
"date_confidence": "verify",
|
|
217
221
|
"detect": {
|
|
@@ -228,6 +232,7 @@
|
|
|
228
232
|
"title": "Programmable Chat API",
|
|
229
233
|
"severity": "high",
|
|
230
234
|
"match": "pattern",
|
|
235
|
+
"applies_to": ["py", "js", "ts", "jsx", "tsx", "mjs"],
|
|
231
236
|
"sunset_date": "2022-07-25",
|
|
232
237
|
"detect": {
|
|
233
238
|
"sdk": ["twilio", "twilio-chat"],
|
|
@@ -243,6 +248,7 @@
|
|
|
243
248
|
"title": "AWS SDK for JavaScript v2",
|
|
244
249
|
"severity": "medium",
|
|
245
250
|
"match": "sdk",
|
|
251
|
+
"applies_to": ["js", "ts", "jsx", "tsx", "mjs"],
|
|
246
252
|
"sunset_date": "2025-09-08",
|
|
247
253
|
"detect": {
|
|
248
254
|
"sdk": ["aws-sdk"],
|
|
@@ -258,6 +264,7 @@
|
|
|
258
264
|
"title": "Legacy API keys (hapikey)",
|
|
259
265
|
"severity": "high",
|
|
260
266
|
"match": "pattern",
|
|
267
|
+
"applies_to": ["py", "js", "ts", "jsx", "tsx", "mjs"],
|
|
261
268
|
"sunset_date": "2022-11-30",
|
|
262
269
|
"detect": {
|
|
263
270
|
"sdk": ["@hubspot/api-client"],
|
|
@@ -266,5 +273,87 @@
|
|
|
266
273
|
"migration_url": "https://developers.hubspot.com/changelog/upcoming-api-key-sunset",
|
|
267
274
|
"summary": "HubSpot deprecated legacy API keys (the hapikey query param) on Nov 30, 2022; requests using hapikey now fail. Migrate to Private App access tokens (Bearer auth). The eCommerce Bridge and Accounting Extension APIs were also sunset Dec 1, 2022.",
|
|
268
275
|
"source": "https://developers.hubspot.com/changelog/upcoming-api-key-sunset"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"id": "openai-python-v0-syntax",
|
|
279
|
+
"vendor": "OpenAI",
|
|
280
|
+
"title": "Legacy openai-python v0 call syntax",
|
|
281
|
+
"severity": "high",
|
|
282
|
+
"match": "pattern",
|
|
283
|
+
"applies_to": ["py"],
|
|
284
|
+
"sunset_date": "2023-11-06",
|
|
285
|
+
"detect": {
|
|
286
|
+
"sdk": ["openai"],
|
|
287
|
+
"patterns": [
|
|
288
|
+
"openai\\.ChatCompletion",
|
|
289
|
+
"openai\\.Completion\\.create",
|
|
290
|
+
"openai\\.Embedding\\.create",
|
|
291
|
+
"openai\\.Moderation\\.create"
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
"migration_url": "https://github.com/openai/openai-python/discussions/742",
|
|
295
|
+
"summary": "openai-python v1.0 (Nov 2023) removed module-level calls. openai.ChatCompletion/Completion/Embedding.create now raise APIRemovedInV1. Instantiate a client (client = OpenAI(); client.chat.completions.create(...)) or run `openai migrate`.",
|
|
296
|
+
"source": "https://github.com/openai/openai-python/issues/2172"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"id": "vercel-ai-sdk-v5-removed",
|
|
300
|
+
"vendor": "Vercel (AI SDK)",
|
|
301
|
+
"title": "AI SDK v4→v5/v6 removed APIs",
|
|
302
|
+
"severity": "medium",
|
|
303
|
+
"match": "pattern",
|
|
304
|
+
"applies_to": ["js", "ts", "jsx", "tsx", "mjs"],
|
|
305
|
+
"sunset_date": "2025-07-31",
|
|
306
|
+
"detect": {
|
|
307
|
+
"sdk": ["ai"],
|
|
308
|
+
"patterns": [
|
|
309
|
+
"from ['\"]ai/react['\"]",
|
|
310
|
+
"from ['\"]ai/openai['\"]",
|
|
311
|
+
"experimental_streamText",
|
|
312
|
+
"experimental_generateText",
|
|
313
|
+
"StreamingTextResponse"
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
"migration_url": "https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0",
|
|
317
|
+
"summary": "AI SDK v5 (Jul 2025) removed deprecated APIs: 'ai/react' → '@ai-sdk/react', experimental_streamText → streamText, StreamingTextResponse removed, useChat maxSteps removed. v6 moved provider imports ('ai/openai' → '@ai-sdk/openai').",
|
|
318
|
+
"source": "https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"id": "langchain-legacy-imports",
|
|
322
|
+
"vendor": "LangChain",
|
|
323
|
+
"title": "Legacy LangChain imports & chains",
|
|
324
|
+
"severity": "medium",
|
|
325
|
+
"match": "pattern",
|
|
326
|
+
"applies_to": ["py"],
|
|
327
|
+
"sunset_date": "2024-05-01",
|
|
328
|
+
"detect": {
|
|
329
|
+
"sdk": ["langchain"],
|
|
330
|
+
"patterns": [
|
|
331
|
+
"from langchain\\.llms import",
|
|
332
|
+
"from langchain\\.chat_models import",
|
|
333
|
+
"from langchain\\.embeddings import",
|
|
334
|
+
"initialize_agent",
|
|
335
|
+
"LLMChain"
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
"migration_url": "https://python.langchain.com/docs/versions/v0_2/",
|
|
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).",
|
|
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"
|
|
269
358
|
}
|
|
270
359
|
]
|