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 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 `[A-Za-z0-9._-]*` version/suffix, then the matching closing quote. So `"gpt-4o"`, `'gpt-4o'`, `` `gpt-4o` ``, and `"gpt-4o-2024-05-13"` match but the same name sitting in prose, JSX, or a comment does **not**.
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
- | `--fail-on <severity>` | Exit non-zero if findings meet a level: `high` \| `medium` \| `low` \| `any` \| `none` (default `none`) |
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` `--fail-on` threshold met · `2` bad path or dataset error. The `--fail-on` flag makes `arol-ai` useful as a CI gate:
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 --fail-on high
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 "" if no fixed date
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 `--fail-on`. |
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`. Use `""` for unmaintained/no-fixed-date items; the report shows a relative hint (e.g. *"in 42 days"* / *"passed 12 days ago"*). |
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, with an optional version suffix). Use this for model ids so prose/JSX mentions don't false-positive. Write the raw name (e.g. `gpt-4.5-preview`) — escaping is automatic. |
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. The optional suffix means `gpt-4o` also catches `"gpt-4o-2024-05-13"`, so pick a family specific enough not to swallow a non-deprecated successor.
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
- const SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
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, { color: shouldUseColor(opts.color) });
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
- // Optional CI gate: exit non-zero if a finding meets/exceeds the threshold.
128
- const failOn = opts.failOn?.toLowerCase();
129
- if (failOn && failOn !== "none") {
130
- const threshold = failOn === "any"
131
- ? 1
132
- : SEVERITY_RANK[failOn] ?? Number.POSITIVE_INFINITY;
133
- const tripped = result.findings.some((f) => SEVERITY_RANK[f.deprecation.severity] >= threshold);
134
- if (tripped)
135
- process.exitCode = 1;
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("--fail-on <severity>", "exit non-zero if findings meet this level: high | medium | low | any | none", "none")
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: typeof r.sunset_date === "string" ? r.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
- /** Render the sunset date with a relative hint, or a note when there is no date. */
44
- function sunsetPhrase(s, sunsetDate, now) {
45
- if (!sunsetDate) {
46
- return s.gray("no fixed sunset date — already deprecated / unmaintained");
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
- const parsed = new Date(`${sunsetDate}T00:00:00Z`);
49
- if (Number.isNaN(parsed.getTime())) {
50
- return s.gray(`sunsets ${sunsetDate}`);
58
+ if (status === "retired") {
59
+ const ago = Math.abs(days);
60
+ return s.red(`retired ${d.sunset_date} (${dayCount(ago)} ago)`);
51
61
  }
52
- const msPerDay = 24 * 60 * 60 * 1000;
53
- const days = Math.round((parsed.getTime() - now.getTime()) / msPerDay);
54
- let rel;
55
- if (days > 1)
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/earliest sunset first; undated last.
102
- const da = a.deprecation.sunset_date || "9999-99-99";
103
- const db = b.deprecation.sunset_date || "9999-99-99";
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(` ${sunsetPhrase(s, d.sunset_date, now)}`);
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
- /** Severity-aware closing CTA, visually separated from the findings above. */
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
- let message;
167
- if (findings.some((f) => f.deprecation.severity === "high")) {
168
- // Prominent: high-severity items break on fixed dates.
169
- message = s.bold(s.red(`⚠ These break on fixed dates. Get alerted before the next one hits you → ${brand}`));
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 optional version/suffix, then
95
- * the SAME closing quote. So a quoted model id (single, double, or backtick)
96
- * and its versioned snapshots match, but never a bare occurrence in
97
- * prose/JSX/markdown.
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)}[A-Za-z0-9._-]*\\1`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arol-ai",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Scan a local repo for upcoming third-party API/SDK deprecations. Fully local — no network, no telemetry, your code never leaves the machine.",
5
5
  "keywords": [
6
6
  "deprecation",
@@ -128,32 +128,20 @@
128
128
  "source": "https://firebase.google.com/docs/ai-logic/models"
129
129
  },
130
130
  {
131
- "id": "openai-gpt4-family-shutdown",
131
+ "id": "openai-retired-gpt4-variants",
132
132
  "vendor": "OpenAI",
133
- "title": "GPT-4 family models (API shutdown)",
133
+ "title": "Retired GPT-4 preview/variant models",
134
134
  "severity": "high",
135
135
  "match": "pattern",
136
136
  "applies_to": ["*"],
137
- "sunset_date": "2026-10-23",
138
- "date_confidence": "verify",
137
+ "sunset_date": "2025-07-14",
139
138
  "detect": {
140
139
  "sdk": ["openai"],
141
- "patterns": [],
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": "The GPT-4 family (gpt-4o snapshots, gpt-4-turbo, gpt-4-0613, o4-mini, etc.) is reported on a single API shutdown date of Oct 23, 2026; calls will then fail. Migrate to the GPT-5 family. VERIFY exact date and model membership on the official deprecations page before shipping.",
156
- "source": "https://platform.openai.com/docs/deprecations"
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
  ]