arol-ai 0.1.0 → 0.1.2

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
@@ -35,25 +35,28 @@ arol-ai scan
35
35
 
36
36
  ```
37
37
  arol · local deprecation scan
38
- Scanned 128 files · 3 APIs detected
38
+ Scanned 128 files · 1 API detected
39
39
 
40
- 3 deprecations found (2 high, 1 medium)
40
+ 1 deprecation found (1 high, 0 medium, 0 low)
41
41
 
42
- AWS · AWS SDK for JavaScript v2 end-of-support HIGH
43
- sunsets 2025-09-08 (passed 269 days ago)
44
- The monolithic 'aws-sdk' (v2) entered maintenance mode in 2024 and reached
45
- end-of-support. Migrate to the modular AWS SDK for JavaScript v3.
42
+ OpenAI · Assistants API (beta) HIGH
43
+ sunsets 2026-08-26 (in 84 days)
44
+ The Assistants API beta is being removed on Aug 26, 2026; requests to
45
+ /v1/assistants and /v1/threads will fail. Migrate to the Responses API +
46
+ Conversations API.
46
47
  found in:
47
- package.json aws-sdk@^2.1400.0
48
- src/storage.ts:1, 42
49
- → migrate: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrating-to-v3.html
48
+ src/agents/run.ts:42 beta.assistants
49
+ src/agents/run.ts:88 → beta.threads
50
+ → migrate: https://platform.openai.com/docs/assistants/migration
50
51
 
51
- ...
52
-
53
- These are today's deprecations. New ones land constantly — get
54
- alerted before the next one breaks you → arol.ai
52
+ ────────────────────────────────────────────────────────────
53
+ ⚠ These break on fixed dates. Get alerted before the next one hits you → arol.ai
55
54
  ```
56
55
 
56
+ Note the citations point at the **exact source lines that use the deprecated API**, not at the manifest. Having the `openai` package installed is not enough on its own — your code has to actually call the removed surface.
57
+
58
+ The closing line is **severity-aware**: a high-severity finding gets the prominent warning above; findings with no high-severity items get `Get continuous deprecation alerts for your stack → arol.ai`; and a clean scan gets `✓ Clean today — but new deprecations land constantly. Stay covered → arol.ai`.
59
+
57
60
  When nothing is found:
58
61
 
59
62
  ```
@@ -62,20 +65,39 @@ When nothing is found:
62
65
 
63
66
  ## How detection works
64
67
 
65
- For every entry in the dataset, `arol-ai` runs two independent checks:
68
+ Detection keys on **actual usage, not mere SDK presence.** Each dataset entry declares a `match` mode that decides what triggers it.
69
+
70
+ ### `match: "pattern"` — the default
71
+
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
+
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**.
76
+
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
+
79
+ 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
+
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.).
82
+
83
+ **Files scanned / skipped**
84
+
85
+ - Extensions scanned: `.js .mjs .cjs .jsx .ts .mts .cts .tsx .py .go`
86
+ - Skipped directories: `node_modules`, `.git`, `dist`, `build`, `.next`, `out`, `coverage`, `.venv`, `venv`, `vendor`
87
+ - Skipped by default: `.md`, `.mdx`, `.txt` (docs/prose, where model names appear as text), plus the tool's own `deprecations.json` and `arol.config.*` / `.arolignore` files.
88
+ - Add a **`.arolignore`** file (gitignore-style globs) at the repo root, and/or pass **`--ignore <glob>`** (repeatable) to skip more paths.
89
+
90
+ ### `match: "sdk"`
91
+
92
+ Flags when a `detect.sdk` package appears in a manifest, **regardless of code** — for the rare "this whole SDK / version line is end-of-life" case.
66
93
 
67
- 1. **Manifest scan** — parses the repo's root manifests for declared dependencies and their versions:
68
- - `package.json` (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, plus simple npm `workspaces`)
69
- - `requirements.txt` (Python)
70
- - `go.mod` (Go)
94
+ ### `match: "version"`
71
95
 
72
- A dependency matches if its name equals one of the entry's `detect.sdk` names (case-insensitively, with PyPI-style `_ . -` normalization).
96
+ Flags when a `detect.sdk` package appears in a manifest **and** its declared version satisfies the entry's `version_range` (e.g. `"<3.0.0"`). Semver-style compare for npm; best-effort numeric compare for pip/go. If `version_range` is omitted, it behaves like `"sdk"`. *(No version entries ship today.)*
73
97
 
74
- 2. **Inline scan** walks source files and regex-matches each entry's `detect.patterns`, recording the file paths and line numbers.
75
- - Extensions scanned: `.js .mjs .cjs .jsx .ts .mts .cts .tsx .py .go`
76
- - Skipped directories: `node_modules`, `.git`, `dist`, `build`, `.next`, `out`, `coverage`, `.venv`, `venv`, `vendor`
98
+ **Manifests parsed** (used by `sdk`/`version` modes): `package.json` (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, plus simple npm `workspaces`), `requirements.txt` (Python), and `go.mod` (Go). A dependency matches a `detect.sdk` name case-insensitively, with PyPI-style `_ . -` normalization.
77
99
 
78
- A deprecation is **detected** if its SDK is present **OR** any of its patterns match. Both kinds of evidence are shown in the report.
100
+ The report cites **source code locations** for `pattern` findings, and the **manifest line** (`package.json name@version`) for `sdk`/`version` findings.
79
101
 
80
102
  ## CLI
81
103
 
@@ -89,6 +111,7 @@ arol-ai scan [path] [options]
89
111
  | `--json` | Output machine-readable JSON instead of the report |
90
112
  | `--no-color` | Disable colored output (also respects `NO_COLOR`) |
91
113
  | `--data <file>` | Use a custom `deprecations.json` instead of the bundled one |
114
+ | `--ignore <glob>` | Skip files matching this glob; repeatable. Combined with `.arolignore`. e.g. `--ignore 'docs/**' --ignore '**/*.gen.ts'` |
92
115
  | `--fail-on <severity>` | Exit non-zero if findings meet a level: `high` \| `medium` \| `low` \| `any` \| `none` (default `none`) |
93
116
  | `-v, --version` | Print the version |
94
117
  | `-h, --help` | Show help |
@@ -110,32 +133,66 @@ All detections are **data-driven** — the bundled dataset lives at
110
133
 
111
134
  ### Schema
112
135
 
136
+ The dataset is either a bare array of entries, or a `{ "deprecations": [ ... ] }` object. A typical `pattern` entry:
137
+
138
+ ```jsonc
139
+ {
140
+ "id": "openai-assistants-api", // unique, stable identifier (required)
141
+ "vendor": "OpenAI", // who owns the API (required)
142
+ "title": "Assistants API (beta)", // short headline (required)
143
+ "severity": "high", // "high" | "medium" | "low" (required)
144
+ "match": "pattern", // "pattern" (default) | "sdk" | "version"
145
+ "sunset_date": "2026-08-26", // ISO YYYY-MM-DD, or "" if no fixed date
146
+ "detect": {
147
+ "sdk": ["openai"], // scope hint for "pattern"; the trigger for "sdk"/"version"
148
+ "patterns": [ // raw regexes: identifiers, endpoints, params
149
+ "beta\\.assistants",
150
+ "beta\\.threads",
151
+ "/v1/assistants"
152
+ ],
153
+ "models": [] // model ids matched only inside string literals
154
+ },
155
+ "migration_url": "https://platform.openai.com/docs/assistants/migration",
156
+ "summary": "One or two sentences explaining the change and what to do."
157
+ }
158
+ ```
159
+
160
+ A model-retirement entry uses `detect.models` so it only fires on a quoted model id, never on prose:
161
+
113
162
  ```jsonc
114
163
  {
115
- "schema_version": 1, // optional, informational
116
- "updated": "2026-06-01", // optional, informational
117
- "deprecations": [
118
- {
119
- "id": "aws-sdk-js-v2-eos", // unique, stable identifier (required)
120
- "vendor": "AWS", // who owns the API (required)
121
- "title": "AWS SDK for JavaScript v2 end-of-support", // short headline (required)
122
- "severity": "high", // "high" | "medium" | "low" (required)
123
- "sunset_date": "2025-09-08",// ISO YYYY-MM-DD, or "" if no fixed date
124
- "detect": {
125
- "sdk": ["aws-sdk"], // dependency/module names to find in manifests
126
- "patterns": [ // regex strings matched against source files
127
- "require\\(\\s*['\"]aws-sdk['\"]\\s*\\)",
128
- "from\\s+['\"]aws-sdk['\"]"
129
- ]
130
- },
131
- "migration_url": "https://docs.aws.amazon.com/.../migrating-to-v3.html",
132
- "summary": "One or two sentences explaining the change and what to do."
133
- }
134
- ]
164
+ "id": "openai-gpt4-family-shutdown",
165
+ "vendor": "OpenAI",
166
+ "title": "GPT-4 family models (API shutdown)",
167
+ "severity": "high",
168
+ "match": "pattern",
169
+ "sunset_date": "2026-10-23",
170
+ "detect": {
171
+ "sdk": ["openai"],
172
+ "patterns": [],
173
+ "models": ["gpt-4o", "gpt-4-turbo", "o4-mini", "gpt-4.5-preview"]
174
+ },
175
+ "migration_url": "https://platform.openai.com/docs/deprecations",
176
+ "summary": "Migrate to the GPT-5 family."
135
177
  }
136
178
  ```
137
179
 
138
- A bare top-level array (`[ { ...entry }, ... ]`) is also accepted.
180
+ A `version` entry instead flags on the installed SDK version (no patterns needed):
181
+
182
+ ```jsonc
183
+ {
184
+ "id": "example-sdk-v2-eol",
185
+ "vendor": "Example",
186
+ "title": "example-sdk v2 line end-of-life",
187
+ "severity": "medium",
188
+ "match": "version",
189
+ "version_range": "<3.0.0", // flags only when the declared version is in range
190
+ "sunset_date": "",
191
+ "detect": { "sdk": ["example-sdk"], "patterns": [] },
192
+ "migration_url": "https://example.com/migrate",
193
+ "summary": "Upgrade example-sdk to v3+."
194
+ }
195
+ ```
139
196
 
140
197
  ### Field reference
141
198
 
@@ -145,19 +202,24 @@ A bare top-level array (`[ { ...entry }, ... ]`) is also accepted.
145
202
  | `vendor` | string | ✓ | Displayed before the title. |
146
203
  | `title` | string | ✓ | Short headline for the finding. |
147
204
  | `severity` | `"high"` \| `"medium"` \| `"low"` | ✓ | Drives color, sort order, and `--fail-on`. |
205
+ | `match` | `"pattern"` \| `"sdk"` \| `"version"` | – | How the entry is triggered. **Defaults to `"pattern"`** when omitted. See [How detection works](#how-detection-works). |
206
+ | `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"`. |
148
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"*). |
149
- | `detect.sdk` | string[] | – | Manifest dependency/module names. May be empty for pattern-only detection. |
150
- | `detect.patterns` | string[] | – | **JSON-escaped** regular-expression strings (so `\d` becomes `\\d`). Matched per source file; invalid regexes are skipped safely. May be empty for manifest-only detection. |
208
+ | `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
+ | `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. |
151
211
  | `migration_url` | string | – | Link shown in the report. |
152
212
  | `summary` | string | – | One or two sentences of guidance. |
153
213
 
154
- > An entry with **both** `detect.sdk` and `detect.patterns` empty can never match and is ignored.
214
+ > A `pattern` entry needs at least one `detect.patterns` **or** `detect.models` entry; an `sdk`/`version` entry needs at least one `detect.sdk`. Entries that can never fire are dropped at load time.
155
215
 
156
- ### Writing good patterns
216
+ ### Writing good patterns & models
157
217
 
158
- - Patterns are matched **case-sensitively** with the global flag over each file's contents; line numbers are reported.
159
- - Escape backslashes for JSON: a regex `\bimport\b` is written `"\\bimport\\b"`.
160
- - Prefer specific anchors (`require\(\s*['"]name['"]\s*\)`, `from\s+['"]name['"]`) over bare package names to avoid false positives.
218
+ - **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.
220
+ - 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
+ - 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
+ - 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.)
161
223
  - Avoid `^`/`$` line anchors — matching runs against the whole file, not line-by-line; use `\b` word boundaries instead.
162
224
 
163
225
  ## Development
package/dist/cli.js CHANGED
@@ -62,6 +62,10 @@ function shouldUseColor(colorFlag) {
62
62
  return Boolean(process.stdout.isTTY);
63
63
  }
64
64
  const SEVERITY_RANK = { high: 3, medium: 2, low: 1 };
65
+ /** Commander collector so --ignore can be passed multiple times. */
66
+ function collectIgnore(value, previous) {
67
+ return previous.concat([value]);
68
+ }
65
69
  function runScan(targetPath, opts) {
66
70
  const root = path.resolve(targetPath ?? ".");
67
71
  // Validate the target directory up front for a friendly error.
@@ -88,7 +92,10 @@ function runScan(targetPath, opts) {
88
92
  process.exitCode = 2;
89
93
  return;
90
94
  }
91
- const result = (0, scanner_1.scanRepo)(root, deprecations);
95
+ const result = (0, scanner_1.scanRepo)(root, deprecations, {
96
+ ignore: opts.ignore,
97
+ dataPath: opts.data,
98
+ });
92
99
  if (opts.json) {
93
100
  const counts = { high: 0, medium: 0, low: 0 };
94
101
  for (const f of result.findings)
@@ -103,6 +110,7 @@ function runScan(targetPath, opts) {
103
110
  vendor: f.deprecation.vendor,
104
111
  title: f.deprecation.title,
105
112
  severity: f.deprecation.severity,
113
+ match: f.deprecation.match,
106
114
  sunset_date: f.deprecation.sunset_date,
107
115
  migration_url: f.deprecation.migration_url,
108
116
  summary: f.deprecation.summary,
@@ -141,6 +149,7 @@ function main(argv) {
141
149
  .option("--json", "output machine-readable JSON instead of the report")
142
150
  .option("--no-color", "disable colored output")
143
151
  .option("--data <file>", "use a custom deprecations.json dataset instead of the bundled one")
152
+ .option("--ignore <glob>", "skip files matching this glob (repeatable); also reads .arolignore", collectIgnore, [])
144
153
  .option("--fail-on <severity>", "exit non-zero if findings meet this level: high | medium | low | any | none", "none")
145
154
  .action((pathArg, options) => {
146
155
  runScan(pathArg, options);
package/dist/data.js CHANGED
@@ -37,6 +37,7 @@ exports.loadDeprecations = loadDeprecations;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const SEVERITIES = ["high", "medium", "low"];
40
+ const MATCH_MODES = ["pattern", "sdk", "version"];
40
41
  /**
41
42
  * Locate the bundled deprecations.json. Tries several candidate locations so the
42
43
  * tool works both when running the compiled output (dist/) from a published
@@ -80,16 +81,29 @@ function coerceDeprecation(raw) {
80
81
  const detect = r.detect;
81
82
  const sdk = detect && isStringArray(detect.sdk) ? detect.sdk : [];
82
83
  const patterns = detect && isStringArray(detect.patterns) ? detect.patterns : [];
83
- // An entry with neither SDKs nor patterns can never match — drop it.
84
- if (sdk.length === 0 && patterns.length === 0)
84
+ const models = detect && isStringArray(detect.models) ? detect.models : [];
85
+ // Default to "pattern" detection keys on real usage, not SDK presence.
86
+ const match = MATCH_MODES.includes(r.match)
87
+ ? r.match
88
+ : "pattern";
89
+ const version_range = isNonEmptyString(r.version_range)
90
+ ? r.version_range
91
+ : undefined;
92
+ // Drop entries that can never fire under their match mode.
93
+ // A "pattern" entry fires on either a raw pattern OR a model-string match.
94
+ if (match === "pattern" && patterns.length === 0 && models.length === 0)
95
+ return null;
96
+ if ((match === "sdk" || match === "version") && sdk.length === 0)
85
97
  return null;
86
98
  return {
87
99
  id: r.id,
88
100
  vendor: r.vendor,
89
101
  title: r.title,
90
102
  severity: r.severity,
103
+ match,
91
104
  sunset_date: typeof r.sunset_date === "string" ? r.sunset_date : "",
92
- detect: { sdk, patterns },
105
+ detect: { sdk, patterns, models },
106
+ version_range,
93
107
  migration_url: typeof r.migration_url === "string" ? r.migration_url : "",
94
108
  summary: typeof r.summary === "string" ? r.summary : "",
95
109
  };
package/dist/report.js CHANGED
@@ -67,22 +67,24 @@ function sunsetPhrase(s, sunsetDate, now) {
67
67
  // Past or imminent sunsets are the urgent ones.
68
68
  return days <= 30 ? s.red(base + hint) : s.yellow(base + hint);
69
69
  }
70
- /** Group pattern matches by file into "path:line, line" summaries. */
70
+ /** One line per source location: "path:line → matched text". */
71
71
  function formatPatternMatches(s, finding) {
72
- const byFile = new Map();
73
- for (const pm of finding.patternMatches) {
74
- const arr = byFile.get(pm.file) ?? [];
75
- arr.push(pm.line);
76
- byFile.set(pm.file, arr);
77
- }
78
- const lines = [];
79
- for (const [file, nums] of byFile) {
80
- const uniqueSorted = Array.from(new Set(nums)).sort((a, b) => a - b);
81
- const shown = uniqueSorted.slice(0, 8);
82
- const more = uniqueSorted.length > shown.length
83
- ? s.gray(` +${uniqueSorted.length - shown.length} more`)
84
- : "";
85
- lines.push(`${s.cyan(file)}${s.gray(":")}${shown.join(s.gray(", "))}${more}`);
72
+ // De-duplicate identical file:line:text triples and order by file then line.
73
+ const seen = new Set();
74
+ const items = finding.patternMatches.filter((pm) => {
75
+ const key = `${pm.file}:${pm.line}:${pm.text}`;
76
+ if (seen.has(key))
77
+ return false;
78
+ seen.add(key);
79
+ return true;
80
+ });
81
+ items.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
82
+ const MAX = 12;
83
+ const shown = items.slice(0, MAX);
84
+ const lines = shown.map((pm) => `${s.cyan(pm.file)}${s.gray(":")}${pm.line} ${s.gray("→")} ${pm.text}`);
85
+ const extra = items.length - shown.length;
86
+ if (extra > 0) {
87
+ lines.push(s.gray(`+${extra} more location${extra === 1 ? "" : "s"}`));
86
88
  }
87
89
  return lines;
88
90
  }
@@ -112,7 +114,7 @@ function renderReport(result, opts) {
112
114
  if (findings.length === 0) {
113
115
  out.push(s.green(s.bold("✓ No upcoming deprecations detected in your stack.")));
114
116
  out.push("");
115
- out.push(footer(s));
117
+ out.push(footer(s, findings));
116
118
  return out.join("\n");
117
119
  }
118
120
  // Severity summary.
@@ -154,15 +156,29 @@ function renderReport(result, opts) {
154
156
  }
155
157
  out.push("");
156
158
  }
157
- out.push(footer(s));
159
+ out.push(footer(s, findings));
158
160
  return out.join("\n");
159
161
  }
160
- function footer(s) {
161
- return [
162
- s.dim("─".repeat(60)),
163
- s.dim("These are today's deprecations. New ones land constantly — get"),
164
- s.dim("alerted before the next one breaks you → ") + s.cyan(s.bold("arol.ai")),
165
- ].join("\n");
162
+ /** Severity-aware closing CTA, visually separated from the findings above. */
163
+ function footer(s, findings) {
164
+ const sep = s.dim("─".repeat(60));
165
+ 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));
180
+ }
181
+ return [sep, message].join("\n");
166
182
  }
167
183
  /** Soft-wrap text to a width, indenting continuation lines. */
168
184
  function wrapText(text, width, indent) {
package/dist/scanner.js CHANGED
@@ -67,13 +67,42 @@ const IGNORED_DIRS = [
67
67
  "venv",
68
68
  "vendor",
69
69
  ];
70
+ /**
71
+ * Files always skipped by default: documentation/prose (where model names show
72
+ * up as text, not code) and the tool's own dataset / config files.
73
+ */
74
+ const DEFAULT_FILE_IGNORES = [
75
+ "**/*.md",
76
+ "**/*.mdx",
77
+ "**/*.txt",
78
+ "**/deprecations.json",
79
+ "**/.arolignore",
80
+ "**/arol.config.*",
81
+ ];
70
82
  /** Skip files larger than this (bytes) to keep the scan fast. */
71
83
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
72
84
  /** Cap matches recorded per pattern per file to avoid pathological output. */
73
85
  const MAX_MATCHES_PER_PATTERN_PER_FILE = 50;
86
+ /** Any of the three string-literal quote characters. */
87
+ const QUOTE_CLASS = "['\"`]";
88
+ /** Escape regex metacharacters in a literal model family name. */
89
+ function escapeRegex(s) {
90
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ }
92
+ /**
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.
98
+ */
99
+ function modelRegexSource(family) {
100
+ return `(${QUOTE_CLASS})${escapeRegex(family)}[A-Za-z0-9._-]*\\1`;
101
+ }
74
102
  function compileDeprecations(deprecations) {
75
103
  return deprecations.map((deprecation) => {
76
104
  const regexes = [];
105
+ // Raw patterns — code identifiers, endpoints, params.
77
106
  for (const pattern of deprecation.detect.patterns) {
78
107
  try {
79
108
  // Global so we can iterate every match and derive line numbers.
@@ -83,6 +112,15 @@ function compileDeprecations(deprecations) {
83
112
  // A malformed pattern in the dataset must not crash the scan.
84
113
  }
85
114
  }
115
+ // Model names — only matched inside string literals (quote-anchored).
116
+ for (const family of deprecation.detect.models) {
117
+ try {
118
+ regexes.push(new RegExp(modelRegexSource(family), "g"));
119
+ }
120
+ catch {
121
+ // Defensive: a pathological family name must not crash the scan.
122
+ }
123
+ }
86
124
  return { deprecation, regexes };
87
125
  });
88
126
  }
@@ -133,9 +171,9 @@ function scanContent(content, relPath, compiled, sink) {
133
171
  if (seenLines.has(line))
134
172
  continue; // one record per line per pattern
135
173
  seenLines.add(line);
136
- const lineStart = lineStarts[line - 1];
137
- const nextStart = line < lineStarts.length ? lineStarts[line] : content.length;
138
- const text = content.slice(lineStart, nextStart).replace(/\r?\n$/, "").trim();
174
+ // Cite the matched substring itself, normalized and length-capped, so
175
+ // the report points at exactly what triggered the finding.
176
+ const text = (m[0] ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
139
177
  if (!recorded) {
140
178
  recorded = [];
141
179
  sink.set(deprecation.id, recorded);
@@ -171,18 +209,121 @@ function matchManifests(deprecations, refs) {
171
209
  }
172
210
  return byId;
173
211
  }
212
+ /** Best-effort parse of a dotted numeric version from a declared string. */
213
+ function parseVersionNumbers(raw) {
214
+ if (!raw)
215
+ return null;
216
+ // Pull the first dotted-number run, ignoring ^ ~ >= <= operators and a "v" prefix.
217
+ const m = /(\d+(?:\.\d+)*)/.exec(raw);
218
+ if (!m)
219
+ return null;
220
+ return m[1].split(".").map((n) => parseInt(n, 10));
221
+ }
222
+ function compareVersions(a, b) {
223
+ const len = Math.max(a.length, b.length);
224
+ for (let i = 0; i < len; i++) {
225
+ const x = a[i] ?? 0;
226
+ const y = b[i] ?? 0;
227
+ if (x !== y)
228
+ return x < y ? -1 : 1;
229
+ }
230
+ return 0;
231
+ }
232
+ /**
233
+ * Best-effort check that a declared version satisfies a simple range such as
234
+ * "<3.0.0", ">=1.2.0", or "=2.1.0" / "2.1.0". Semver-ish: compares dotted numbers.
235
+ * Returns true when no range is given; false when a range is required but the
236
+ * declared version can't be parsed (stay conservative — don't over-flag).
237
+ */
238
+ function versionInRange(declared, range) {
239
+ if (!range)
240
+ return true; // no constraint → presence already established by caller
241
+ const rm = /^\s*(<=|>=|<|>|={1,3})?\s*v?(\d+(?:\.\d+)*)\s*$/.exec(range);
242
+ if (!rm)
243
+ return true; // unparseable range → don't invent a false constraint
244
+ const op = rm[1] || "=";
245
+ const target = rm[2].split(".").map((n) => parseInt(n, 10));
246
+ const have = parseVersionNumbers(declared);
247
+ if (!have)
248
+ return false;
249
+ const cmp = compareVersions(have, target);
250
+ switch (op) {
251
+ case "<":
252
+ return cmp < 0;
253
+ case "<=":
254
+ return cmp <= 0;
255
+ case ">":
256
+ return cmp > 0;
257
+ case ">=":
258
+ return cmp >= 0;
259
+ default:
260
+ return cmp === 0;
261
+ }
262
+ }
263
+ /**
264
+ * Convert one .arolignore line (gitignore-style) into fast-glob ignore globs.
265
+ * Supports comments (#), blank lines, leading "/" anchoring, and trailing "/"
266
+ * for directories. Negations ("!") are not supported and are skipped.
267
+ */
268
+ function arolignoreLineToGlobs(rawLine) {
269
+ let line = rawLine.trim();
270
+ if (!line || line.startsWith("#") || line.startsWith("!"))
271
+ return [];
272
+ const anchored = line.startsWith("/");
273
+ if (anchored)
274
+ line = line.slice(1);
275
+ const isDir = line.endsWith("/");
276
+ if (isDir)
277
+ line = line.replace(/\/+$/, "");
278
+ if (!line)
279
+ return [];
280
+ const base = anchored ? line : `**/${line}`;
281
+ // A directory ignore covers its contents; a file/glob ignore covers both the
282
+ // entry itself and (harmlessly) anything beneath it if it is a directory.
283
+ return isDir ? [`${base}/**`] : [base, `${base}/**`];
284
+ }
285
+ /** Read and parse a repo's .arolignore file into ignore globs (empty if none). */
286
+ function loadArolignore(root) {
287
+ let content;
288
+ try {
289
+ content = fs.readFileSync(path.join(root, ".arolignore"), "utf8");
290
+ }
291
+ catch {
292
+ return [];
293
+ }
294
+ return content.split(/\r?\n/).flatMap(arolignoreLineToGlobs);
295
+ }
174
296
  /**
175
297
  * Scan a repository for deprecation usage.
176
298
  * @param root repo root to scan.
177
299
  * @param deprecations validated dataset entries.
300
+ * @param options optional ignore globs (--ignore) and custom dataset path.
178
301
  */
179
- function scanRepo(root, deprecations) {
302
+ function scanRepo(root, deprecations, options = {}) {
180
303
  const absRoot = path.resolve(root);
181
- // 1. Manifest scan.
304
+ // Assemble the ignore list: dirs + default file skips + .arolignore + --ignore.
305
+ const ignoreGlobs = [
306
+ ...IGNORED_DIRS.map((d) => `**/${d}/**`),
307
+ ...DEFAULT_FILE_IGNORES,
308
+ ...loadArolignore(absRoot),
309
+ ...(options.ignore ?? []),
310
+ ];
311
+ // Never scan the active custom dataset file, even if it lives in the tree.
312
+ if (options.dataPath) {
313
+ const relData = path.relative(absRoot, path.resolve(options.dataPath));
314
+ if (relData && !relData.startsWith("..") && !path.isAbsolute(relData)) {
315
+ ignoreGlobs.push(relData);
316
+ }
317
+ }
318
+ // Partition by match mode: "pattern" entries key on real source usage, while
319
+ // "sdk"/"version" entries key on the manifest.
320
+ const patternDeps = deprecations.filter((d) => d.match === "pattern");
321
+ const manifestDeps = deprecations.filter((d) => d.match === "sdk" || d.match === "version");
322
+ // 1. Manifest scan (drives sdk/version entries; also lists the manifests read).
182
323
  const { refs, manifests } = (0, manifests_1.collectManifestDeps)(absRoot);
183
- const manifestMatches = matchManifests(deprecations, refs);
184
- // 2. Inline scan.
185
- const compiled = compileDeprecations(deprecations);
324
+ const manifestMatches = matchManifests(manifestDeps, refs);
325
+ // 2. Inline source scan (drives pattern entries — usage, not mere presence).
326
+ const compiled = compileDeprecations(patternDeps);
186
327
  const patternSink = new Map();
187
328
  const files = fast_glob_1.default.sync([`**/*.{${SOURCE_EXTENSIONS.join(",")}}`], {
188
329
  cwd: absRoot,
@@ -191,7 +332,7 @@ function scanRepo(root, deprecations) {
191
332
  dot: false,
192
333
  followSymbolicLinks: false,
193
334
  suppressErrors: true,
194
- ignore: IGNORED_DIRS.map((d) => `**/${d}/**`),
335
+ ignore: ignoreGlobs,
195
336
  });
196
337
  let scannedFiles = 0;
197
338
  for (const rel of files) {
@@ -215,14 +356,30 @@ function scanRepo(root, deprecations) {
215
356
  scannedFiles++;
216
357
  scanContent(content, rel, compiled, patternSink);
217
358
  }
218
- // 3. Combine: detected if a manifest match OR a pattern match exists.
359
+ // 3. Build findings one per deprecation, evaluated per its match mode.
219
360
  const findings = [];
220
361
  for (const deprecation of deprecations) {
362
+ if (deprecation.match === "pattern") {
363
+ // Flag ONLY on a real source hit; manifest/SDK presence is irrelevant here.
364
+ const pm = patternSink.get(deprecation.id) ?? [];
365
+ if (pm.length === 0)
366
+ continue;
367
+ findings.push({ deprecation, manifestMatches: [], patternMatches: pm });
368
+ continue;
369
+ }
370
+ // "sdk" / "version": evaluate against the manifest.
221
371
  const mm = manifestMatches.get(deprecation.id) ?? [];
222
- const pm = patternSink.get(deprecation.id) ?? [];
223
- if (mm.length === 0 && pm.length === 0)
372
+ if (mm.length === 0)
373
+ continue;
374
+ if (deprecation.match === "version") {
375
+ const inRange = mm.filter((m) => versionInRange(m.version, deprecation.version_range));
376
+ if (inRange.length === 0)
377
+ continue;
378
+ findings.push({ deprecation, manifestMatches: inRange, patternMatches: [] });
224
379
  continue;
225
- findings.push({ deprecation, manifestMatches: mm, patternMatches: pm });
380
+ }
381
+ // "sdk": mere presence in a manifest is enough.
382
+ findings.push({ deprecation, manifestMatches: mm, patternMatches: [] });
226
383
  }
227
384
  return { scannedFiles, manifestsScanned: manifests, findings };
228
385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arol-ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -4,6 +4,7 @@
4
4
  "vendor": "OpenAI",
5
5
  "title": "Assistants API (beta)",
6
6
  "severity": "high",
7
+ "match": "pattern",
7
8
  "sunset_date": "2026-08-26",
8
9
  "detect": {
9
10
  "sdk": ["openai"],
@@ -23,10 +24,12 @@
23
24
  "vendor": "Anthropic",
24
25
  "title": "Claude Sonnet 4 & Opus 4",
25
26
  "severity": "high",
27
+ "match": "pattern",
26
28
  "sunset_date": "2026-06-15",
27
29
  "detect": {
28
30
  "sdk": ["@anthropic-ai/sdk", "anthropic"],
29
- "patterns": [
31
+ "patterns": [],
32
+ "models": [
30
33
  "claude-sonnet-4-20250514",
31
34
  "claude-opus-4-20250514",
32
35
  "claude-sonnet-4-0",
@@ -42,15 +45,17 @@
42
45
  "vendor": "Anthropic",
43
46
  "title": "Retired Claude models (Haiku 3, 3.5/3.7 Sonnet, Claude 2.x)",
44
47
  "severity": "high",
48
+ "match": "pattern",
45
49
  "sunset_date": "2026-04-20",
46
50
  "detect": {
47
51
  "sdk": ["@anthropic-ai/sdk", "anthropic"],
48
- "patterns": [
52
+ "patterns": [],
53
+ "models": [
49
54
  "claude-3-haiku-20240307",
50
55
  "claude-3-5-sonnet",
51
56
  "claude-3-7-sonnet",
52
- "claude-2\\.1",
53
- "claude-2\\.0",
57
+ "claude-2.1",
58
+ "claude-2.0",
54
59
  "claude-instant"
55
60
  ]
56
61
  },
@@ -63,14 +68,12 @@
63
68
  "vendor": "Google (Gemini)",
64
69
  "title": "Gemini 2.5 Pro / Flash / Flash-Lite",
65
70
  "severity": "high",
71
+ "match": "pattern",
66
72
  "sunset_date": "2026-10-16",
67
73
  "detect": {
68
74
  "sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
69
- "patterns": [
70
- "gemini-2\\.5-pro",
71
- "gemini-2\\.5-flash",
72
- "gemini-2\\.5-flash-lite"
73
- ]
75
+ "patterns": [],
76
+ "models": ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"]
74
77
  },
75
78
  "migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
76
79
  "summary": "Gemini 2.5 models scheduled for shutdown Oct 16, 2026 (Google states this is the earliest possible date). Migrate to Gemini 3.x.",
@@ -81,14 +84,16 @@
81
84
  "vendor": "Google (Gemini)",
82
85
  "title": "Gemini 2.0 Flash family",
83
86
  "severity": "high",
87
+ "match": "pattern",
84
88
  "sunset_date": "2026-06-01",
85
89
  "detect": {
86
90
  "sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
87
- "patterns": [
88
- "gemini-2\\.0-flash",
89
- "gemini-2\\.0-flash-001",
90
- "gemini-2\\.0-flash-lite",
91
- "gemini-2\\.0-flash-lite-001"
91
+ "patterns": [],
92
+ "models": [
93
+ "gemini-2.0-flash",
94
+ "gemini-2.0-flash-001",
95
+ "gemini-2.0-flash-lite",
96
+ "gemini-2.0-flash-lite-001"
92
97
  ]
93
98
  },
94
99
  "migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
@@ -100,18 +105,166 @@
100
105
  "vendor": "Google (Gemini)",
101
106
  "title": "Gemini 1.0 / 1.5 models",
102
107
  "severity": "high",
108
+ "match": "pattern",
103
109
  "sunset_date": "2026-04-29",
104
110
  "detect": {
105
111
  "sdk": ["@google/generative-ai", "@google/genai", "google-generativeai"],
106
- "patterns": [
107
- "gemini-1\\.5-pro",
108
- "gemini-1\\.5-flash",
109
- "gemini-1\\.0-pro",
110
- "gemini-pro"
111
- ]
112
+ "patterns": [],
113
+ "models": ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro", "gemini-pro"]
112
114
  },
113
115
  "migration_url": "https://ai.google.dev/gemini-api/docs/deprecations",
114
116
  "summary": "All Gemini 1.0 and 1.5 models are shut down and return 404. Migrate to Gemini 2.5+ or 3.x.",
115
117
  "source": "https://firebase.google.com/docs/ai-logic/models"
118
+ },
119
+ {
120
+ "id": "openai-gpt4-family-shutdown",
121
+ "vendor": "OpenAI",
122
+ "title": "GPT-4 family models (API shutdown)",
123
+ "severity": "high",
124
+ "match": "pattern",
125
+ "sunset_date": "2026-10-23",
126
+ "date_confidence": "verify",
127
+ "detect": {
128
+ "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
+ ]
141
+ },
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"
145
+ },
146
+ {
147
+ "id": "openai-legacy-retired-models",
148
+ "vendor": "OpenAI",
149
+ "title": "Retired legacy OpenAI models (GPT-3 era, early snapshots)",
150
+ "severity": "high",
151
+ "match": "pattern",
152
+ "sunset_date": "2024-01-04",
153
+ "detect": {
154
+ "sdk": ["openai"],
155
+ "patterns": [],
156
+ "models": [
157
+ "text-davinci-003",
158
+ "text-davinci-002",
159
+ "gpt-3.5-turbo-0301",
160
+ "gpt-3.5-turbo-0613",
161
+ "gpt-4-0314",
162
+ "code-davinci",
163
+ "text-curie",
164
+ "text-babbage"
165
+ ]
166
+ },
167
+ "migration_url": "https://platform.openai.com/docs/deprecations",
168
+ "summary": "These legacy models (GPT-3-era completions and early dated GPT-4/3.5 snapshots) are already retired and return errors. Migrate to current models.",
169
+ "source": "https://platform.openai.com/docs/deprecations"
170
+ },
171
+ {
172
+ "id": "stripe-removed-js-methods",
173
+ "vendor": "Stripe",
174
+ "title": "Removed legacy Stripe.js methods",
175
+ "severity": "high",
176
+ "match": "pattern",
177
+ "sunset_date": "2026-03-25",
178
+ "detect": {
179
+ "sdk": ["@stripe/stripe-js", "stripe"],
180
+ "patterns": [
181
+ "handleCardPayment",
182
+ "confirmPaymentIntent",
183
+ "handleFpxPayment",
184
+ "handleCardSetup",
185
+ "confirmSetupIntent",
186
+ "createSource",
187
+ "retrieveSource"
188
+ ]
189
+ },
190
+ "migration_url": "https://docs.stripe.com/changelog/dahlia/2026-03-25/remove-legacy-stripejs-methods",
191
+ "summary": "These legacy Stripe.js methods were removed and now throw errors. Replace handleCardPayment/confirmPaymentIntent with confirmCardPayment, handleCardSetup/confirmSetupIntent with confirmCardSetup, and migrate createSource/retrieveSource to the PaymentMethods API.",
192
+ "source": "https://docs.stripe.com/changelog/dahlia/2026-03-25/remove-legacy-stripejs-methods"
193
+ },
194
+ {
195
+ "id": "stripe-sources-charges-legacy",
196
+ "vendor": "Stripe",
197
+ "title": "Sources API / legacy Charges API",
198
+ "severity": "medium",
199
+ "match": "pattern",
200
+ "sunset_date": "2024-05-15",
201
+ "detect": {
202
+ "sdk": ["stripe"],
203
+ "patterns": ["\\.sources\\.create", "charges\\.create", "Charge\\.create"]
204
+ },
205
+ "migration_url": "https://docs.stripe.com/payments/older-apis",
206
+ "summary": "The Sources API is deprecated (local payment methods stopped being accepted May 15, 2024) and the Charges API is legacy. Migrate to the PaymentMethods + PaymentIntents APIs.",
207
+ "source": "https://docs.stripe.com/payments/older-apis"
208
+ },
209
+ {
210
+ "id": "twilio-notify-eol",
211
+ "vendor": "Twilio",
212
+ "title": "Notify API",
213
+ "severity": "high",
214
+ "match": "pattern",
215
+ "sunset_date": "2027-01-31",
216
+ "date_confidence": "verify",
217
+ "detect": {
218
+ "sdk": ["twilio"],
219
+ "patterns": ["notify\\.v1", "\\.notify\\.services", "client\\.notify"]
220
+ },
221
+ "migration_url": "https://www.twilio.com/en-us/changelog",
222
+ "summary": "Twilio Notify reaches end of life Jan 31, 2027; after that all Notify API requests will fail. No 1:1 replacement — rebuild with Programmable Messaging / Conversations. Verify the date against Twilio's official EOL notice.",
223
+ "source": "https://www.courier.com/blog/twilio-notify-end-of-life"
224
+ },
225
+ {
226
+ "id": "twilio-programmable-chat-retired",
227
+ "vendor": "Twilio",
228
+ "title": "Programmable Chat API",
229
+ "severity": "high",
230
+ "match": "pattern",
231
+ "sunset_date": "2022-07-25",
232
+ "detect": {
233
+ "sdk": ["twilio", "twilio-chat"],
234
+ "patterns": ["chat\\.v2", "IpMessaging", "twilio-chat"]
235
+ },
236
+ "migration_url": "https://www.twilio.com/docs/conversations/migrating-chat-conversations",
237
+ "summary": "The standalone Programmable Chat API was sunset July 25, 2022 (Programmable Chat in Flex ended June 1, 2026). Migrate to the Conversations API.",
238
+ "source": "https://www.twilio.com/en-us/changelog/programmable-chat-end-of-life-notice"
239
+ },
240
+ {
241
+ "id": "aws-sdk-js-v2-eol",
242
+ "vendor": "AWS",
243
+ "title": "AWS SDK for JavaScript v2",
244
+ "severity": "medium",
245
+ "match": "sdk",
246
+ "sunset_date": "2025-09-08",
247
+ "detect": {
248
+ "sdk": ["aws-sdk"],
249
+ "patterns": []
250
+ },
251
+ "migration_url": "https://aws.amazon.com/blogs/developer/announcing-end-of-support-for-aws-sdk-for-javascript-v2/",
252
+ "summary": "AWS SDK for JavaScript v2 (the 'aws-sdk' package) reached end-of-support Sept 8, 2025 — no more updates or security fixes. Migrate to the modular AWS SDK v3 (@aws-sdk/* packages).",
253
+ "source": "https://aws.amazon.com/blogs/developer/announcing-end-of-support-for-aws-sdk-for-javascript-v2/"
254
+ },
255
+ {
256
+ "id": "hubspot-api-key-hapikey",
257
+ "vendor": "HubSpot",
258
+ "title": "Legacy API keys (hapikey)",
259
+ "severity": "high",
260
+ "match": "pattern",
261
+ "sunset_date": "2022-11-30",
262
+ "detect": {
263
+ "sdk": ["@hubspot/api-client"],
264
+ "patterns": ["hapikey\\s*=", "hapiKey\\s*="]
265
+ },
266
+ "migration_url": "https://developers.hubspot.com/changelog/upcoming-api-key-sunset",
267
+ "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
+ "source": "https://developers.hubspot.com/changelog/upcoming-api-key-sunset"
116
269
  }
117
270
  ]