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 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 `[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
 
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
- | `--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. |
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` `--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:
122
129
 
123
130
  ```sh
124
- 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
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
- "sunset_date": "2026-08-26", // ISO YYYY-MM-DD, or "" if no fixed date
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 `--fail-on`. |
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`. Use `""` for unmaintained/no-fixed-date items; the report shows a relative hint (e.g. *"in 42 days"* / *"passed 12 days ago"*). |
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, 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. |
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. 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.
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
- 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,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
- sunset_date: typeof r.sunset_date === "string" ? r.sunset_date : "",
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
- /** 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) => {
@@ -121,9 +122,110 @@ function compileDeprecations(deprecations) {
121
122
  // Defensive: a pathological family name must not crash the scan.
122
123
  }
123
124
  }
124
- return { deprecation, regexes };
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
- scanContent(content, rel, compiled, patternSink);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arol-ai",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",
@@ -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": ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro", "gemini-pro"]
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-family-shutdown",
131
+ "id": "openai-retired-gpt4-variants",
121
132
  "vendor": "OpenAI",
122
- "title": "GPT-4 family models (API shutdown)",
133
+ "title": "Retired GPT-4 preview/variant models",
123
134
  "severity": "high",
124
135
  "match": "pattern",
125
- "sunset_date": "2026-10-23",
126
- "date_confidence": "verify",
136
+ "applies_to": ["*"],
137
+ "sunset_date": "2025-07-14",
127
138
  "detect": {
128
139
  "sdk": ["openai"],
129
- "patterns": [],
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": "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.",
144
- "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"
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
  ]