arol-ai 0.1.9 → 0.2.0
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/dist/data.js +3 -0
- package/dist/imports.js +111 -0
- package/dist/scanner.js +37 -8
- package/package.json +2 -2
- package/src/data/deprecations.json +45 -2
package/dist/data.js
CHANGED
|
@@ -80,6 +80,9 @@ function coerceDeprecation(raw) {
|
|
|
80
80
|
if (!SEVERITIES.includes(r.severity))
|
|
81
81
|
return null;
|
|
82
82
|
const detect = r.detect;
|
|
83
|
+
// detect.sdk is now FUNCTIONAL for match:"pattern" entries (import-gating, see
|
|
84
|
+
// imports.ts): a non-empty sdk means the entry's patterns only run in files that
|
|
85
|
+
// import a matching package. Empty sdk = ungated (patterns run everywhere).
|
|
83
86
|
const sdk = detect && isStringArray(detect.sdk) ? detect.sdk : [];
|
|
84
87
|
const patterns = detect && isStringArray(detect.patterns) ? detect.patterns : [];
|
|
85
88
|
const models = detect && isStringArray(detect.models) ? detect.models : [];
|
package/dist/imports.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Import extraction for import-gating.
|
|
4
|
+
*
|
|
5
|
+
* A pattern entry with a non-empty detect.sdk only runs its patterns in a file
|
|
6
|
+
* that imports a matching package. We parse imports from the SAME preprocessed
|
|
7
|
+
* (comment-stripped, string-aware) text the patterns use, so comment/string
|
|
8
|
+
* handling stays consistent. Regex-based, one pass per file.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.NO_IMPORTS = void 0;
|
|
12
|
+
exports.isGateableLang = isGateableLang;
|
|
13
|
+
exports.sdkMatchesImport = sdkMatchesImport;
|
|
14
|
+
exports.importsSatisfySdk = importsSatisfySdk;
|
|
15
|
+
exports.extractImports = extractImports;
|
|
16
|
+
/** JS/TS extensions that get full import parsing. */
|
|
17
|
+
const JS_EXTS = new Set(["js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx"]);
|
|
18
|
+
/** A shared empty set for files we don't gate (avoids per-call allocation). */
|
|
19
|
+
exports.NO_IMPORTS = new Set();
|
|
20
|
+
/**
|
|
21
|
+
* Languages where import-gating applies. Go is a v1 fallback (not gated) and
|
|
22
|
+
* unknown extensions are never gated.
|
|
23
|
+
*/
|
|
24
|
+
function isGateableLang(ext) {
|
|
25
|
+
const e = ext.toLowerCase();
|
|
26
|
+
return JS_EXTS.has(e) || e === "py";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An import source matches an sdk name if it equals it, or is a subpath of it.
|
|
30
|
+
* So sdk:["ai"] matches 'ai' and 'ai/test', but NOT 'aimee' or 'ai-utils'.
|
|
31
|
+
* Scoped packages behave the same: '@ai-sdk/openai' matches '@ai-sdk/openai/sub'.
|
|
32
|
+
* (Intentionally stricter and case-sensitive — distinct from manifests.nameMatches.)
|
|
33
|
+
*/
|
|
34
|
+
function sdkMatchesImport(sdkName, source) {
|
|
35
|
+
return source === sdkName || source.startsWith(sdkName + "/");
|
|
36
|
+
}
|
|
37
|
+
/** True if any import source in the set matches any of the entry's sdk names. */
|
|
38
|
+
function importsSatisfySdk(sdkNames, imports) {
|
|
39
|
+
for (const sdk of sdkNames) {
|
|
40
|
+
for (const source of imports) {
|
|
41
|
+
if (sdkMatchesImport(sdk, source))
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Extract the set of imported package sources from preprocessed source text.
|
|
49
|
+
* JS/TS returns raw import specifiers ('ai', 'ai/test', '@ai-sdk/openai').
|
|
50
|
+
* Python returns top-level module names ('openai', 'langchain').
|
|
51
|
+
* Other languages (incl. Go) return an empty set — the caller does not gate them.
|
|
52
|
+
*/
|
|
53
|
+
function extractImports(text, ext) {
|
|
54
|
+
const e = ext.toLowerCase();
|
|
55
|
+
if (e === "py")
|
|
56
|
+
return extractPythonImports(text);
|
|
57
|
+
if (JS_EXTS.has(e))
|
|
58
|
+
return extractJsImports(text);
|
|
59
|
+
return new Set(); // go / unknown — TODO: gate Go imports
|
|
60
|
+
}
|
|
61
|
+
function collect(text, re, group, out) {
|
|
62
|
+
re.lastIndex = 0;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(text)) !== null) {
|
|
65
|
+
if (m.index === re.lastIndex)
|
|
66
|
+
re.lastIndex++; // guard zero-width
|
|
67
|
+
const src = m[group];
|
|
68
|
+
if (src)
|
|
69
|
+
out.add(src);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function extractJsImports(text) {
|
|
73
|
+
const out = new Set();
|
|
74
|
+
// `import ... from '<src>'` and `export ... from '<src>'` — covers named,
|
|
75
|
+
// default, namespace, aliased, `type`, and re-export. Multiline-safe: the
|
|
76
|
+
// binding span excludes ; ' " ` so it can't run across statements or quotes.
|
|
77
|
+
collect(text, /\b(?:import|export)\b[^;'"`]*?\bfrom\s*(['"`])([^'"`]+)\1/g, 2, out);
|
|
78
|
+
// Side-effect import: `import '<src>'` (import directly followed by a quote).
|
|
79
|
+
collect(text, /\bimport\s*(['"`])([^'"`]+)\1/g, 2, out);
|
|
80
|
+
// Dynamic import: `import('<src>')`.
|
|
81
|
+
collect(text, /\bimport\s*\(\s*(['"`])([^'"`]+)\1/g, 2, out);
|
|
82
|
+
// CommonJS: `require('<src>')`.
|
|
83
|
+
collect(text, /\brequire\s*\(\s*(['"`])([^'"`]+)\1/g, 2, out);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
/** Top-level module of a dotted path ("openai.error" -> "openai"); "" for relative. */
|
|
87
|
+
function topLevelModule(mod) {
|
|
88
|
+
return mod.trim().split(".")[0];
|
|
89
|
+
}
|
|
90
|
+
function extractPythonImports(text) {
|
|
91
|
+
const out = new Set();
|
|
92
|
+
let m;
|
|
93
|
+
// `from pkg[.sub] import ...`
|
|
94
|
+
const fromRe = /^[ \t]*from[ \t]+([.\w]+)[ \t]+import\b/gm;
|
|
95
|
+
while ((m = fromRe.exec(text)) !== null) {
|
|
96
|
+
const top = topLevelModule(m[1]);
|
|
97
|
+
if (top)
|
|
98
|
+
out.add(top); // skip relative imports ("from . import x")
|
|
99
|
+
}
|
|
100
|
+
// `import pkg`, `import pkg as p`, `import a, b`
|
|
101
|
+
const importRe = /^[ \t]*import[ \t]+(.+)$/gm;
|
|
102
|
+
while ((m = importRe.exec(text)) !== null) {
|
|
103
|
+
for (const part of m[1].split(",")) {
|
|
104
|
+
const name = part.trim().split(/[ \t]+as[ \t]+/)[0];
|
|
105
|
+
const top = topLevelModule(name);
|
|
106
|
+
if (top)
|
|
107
|
+
out.add(top);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
package/dist/scanner.js
CHANGED
|
@@ -42,6 +42,7 @@ const fs = __importStar(require("fs"));
|
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
44
44
|
const manifests_1 = require("./manifests");
|
|
45
|
+
const imports_1 = require("./imports");
|
|
45
46
|
/** Source file extensions that get the inline regex scan. */
|
|
46
47
|
const SOURCE_EXTENSIONS = [
|
|
47
48
|
"js",
|
|
@@ -103,22 +104,23 @@ function modelRegexSource(family) {
|
|
|
103
104
|
}
|
|
104
105
|
function compileDeprecations(deprecations) {
|
|
105
106
|
return deprecations.map((deprecation) => {
|
|
106
|
-
const
|
|
107
|
+
const patternRegexes = [];
|
|
107
108
|
// Raw patterns — code identifiers, endpoints, params.
|
|
108
109
|
// `?? []` guards against entries not produced by the loader (e.g. tests).
|
|
109
110
|
for (const pattern of deprecation.detect.patterns ?? []) {
|
|
110
111
|
try {
|
|
111
112
|
// Global so we can iterate every match and derive line numbers.
|
|
112
|
-
|
|
113
|
+
patternRegexes.push(new RegExp(pattern, "g"));
|
|
113
114
|
}
|
|
114
115
|
catch {
|
|
115
116
|
// A malformed pattern in the dataset must not crash the scan.
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
// Model names — only matched inside string literals (quote-anchored).
|
|
120
|
+
const modelRegexes = [];
|
|
119
121
|
for (const family of deprecation.detect.models ?? []) {
|
|
120
122
|
try {
|
|
121
|
-
|
|
123
|
+
modelRegexes.push(new RegExp(modelRegexSource(family), "g"));
|
|
122
124
|
}
|
|
123
125
|
catch {
|
|
124
126
|
// Defensive: a pathological family name must not crash the scan.
|
|
@@ -127,7 +129,13 @@ function compileDeprecations(deprecations) {
|
|
|
127
129
|
// Missing/empty applies_to means "applies everywhere" (["*"]), preserved here.
|
|
128
130
|
const declaredExts = deprecation.applies_to ?? [];
|
|
129
131
|
const appliesTo = new Set((declaredExts.length > 0 ? declaredExts : ["*"]).map((e) => e.toLowerCase()));
|
|
130
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
deprecation,
|
|
134
|
+
patternRegexes,
|
|
135
|
+
modelRegexes,
|
|
136
|
+
appliesTo,
|
|
137
|
+
sdkGate: deprecation.detect.sdk ?? [],
|
|
138
|
+
};
|
|
131
139
|
});
|
|
132
140
|
}
|
|
133
141
|
/** True if a compiled entry should be tested against a file with this extension. */
|
|
@@ -256,10 +264,10 @@ function lineNumberAt(lineStarts, offset) {
|
|
|
256
264
|
}
|
|
257
265
|
return ans + 1;
|
|
258
266
|
}
|
|
259
|
-
/** Match
|
|
260
|
-
function scanContent(content, relPath,
|
|
267
|
+
/** Match the selected regexes for each deprecation against one file's contents. */
|
|
268
|
+
function scanContent(content, relPath, runs, sink) {
|
|
261
269
|
const lineStarts = computeLineStarts(content);
|
|
262
|
-
for (const { deprecation, regexes } of
|
|
270
|
+
for (const { deprecation, regexes } of runs) {
|
|
263
271
|
if (regexes.length === 0)
|
|
264
272
|
continue;
|
|
265
273
|
let recorded = sink.get(deprecation.id);
|
|
@@ -470,7 +478,28 @@ function scanRepo(root, deprecations, options = {}) {
|
|
|
470
478
|
// comments don't count. Offsets are preserved, so line numbers stay correct.
|
|
471
479
|
const cfg = commentConfig(ext);
|
|
472
480
|
const scanText = cfg ? stripComments(content, cfg) : content;
|
|
473
|
-
|
|
481
|
+
// Import-gating: parse imports ONCE per file, reused across all entries.
|
|
482
|
+
// Only needed when some applicable entry actually gates (non-empty sdk) and
|
|
483
|
+
// the language supports gating (Go/unknown fall back to ungated). TODO: gate Go.
|
|
484
|
+
const gateable = (0, imports_1.isGateableLang)(ext);
|
|
485
|
+
const needImports = gateable && applicable.some((c) => c.sdkGate.length > 0);
|
|
486
|
+
const imports = needImports ? (0, imports_1.extractImports)(scanText, ext) : imports_1.NO_IMPORTS;
|
|
487
|
+
// For each entry: model regexes always run; pattern regexes run only when the
|
|
488
|
+
// gate is open (empty sdk → always; non-gateable lang → always; else the file
|
|
489
|
+
// must import a matching package).
|
|
490
|
+
const runs = [];
|
|
491
|
+
for (const c of applicable) {
|
|
492
|
+
const gateOpen = c.sdkGate.length === 0 ||
|
|
493
|
+
!gateable ||
|
|
494
|
+
(0, imports_1.importsSatisfySdk)(c.sdkGate, imports);
|
|
495
|
+
const regexes = gateOpen
|
|
496
|
+
? [...c.modelRegexes, ...c.patternRegexes]
|
|
497
|
+
: c.modelRegexes;
|
|
498
|
+
if (regexes.length > 0)
|
|
499
|
+
runs.push({ deprecation: c.deprecation, regexes });
|
|
500
|
+
}
|
|
501
|
+
if (runs.length > 0)
|
|
502
|
+
scanContent(scanText, rel, runs, patternSink);
|
|
474
503
|
}
|
|
475
504
|
// 3. Build findings — one per deprecation, evaluated per its match mode.
|
|
476
505
|
const findings = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arol-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"arol-ai": "^0.1.
|
|
37
|
+
"arol-ai": "^0.1.10",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"fast-glob": "^3.3.2"
|
|
40
40
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"applies_to": ["py", "js", "ts", "jsx", "tsx", "mjs"],
|
|
9
9
|
"sunset_date": "2026-08-26",
|
|
10
10
|
"detect": {
|
|
11
|
-
"sdk": [
|
|
11
|
+
"sdk": [],
|
|
12
12
|
"patterns": [
|
|
13
13
|
"\\bbeta\\.assistants\\b",
|
|
14
14
|
"\\bbeta\\.threads\\b",
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"applies_to": ["py", "js", "ts", "jsx", "tsx", "mjs"],
|
|
287
287
|
"sunset_date": "2022-11-30",
|
|
288
288
|
"detect": {
|
|
289
|
-
"sdk": [
|
|
289
|
+
"sdk": [],
|
|
290
290
|
"patterns": ["hapikey\\s*=", "hapiKey\\s*="]
|
|
291
291
|
},
|
|
292
292
|
"migration_url": "https://developers.hubspot.com/changelog/upcoming-api-key-sunset",
|
|
@@ -474,5 +474,48 @@
|
|
|
474
474
|
"migration_url": "https://platform.openai.com/docs/changelog",
|
|
475
475
|
"summary": "The Realtime API beta was removed from the API on May 12, 2026. Migrate from the client.beta.realtime interface to the released (GA) Realtime API.",
|
|
476
476
|
"source": "https://platform.openai.com/docs/changelog"
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"id": "vercel-ai-sdk-v6-removed",
|
|
480
|
+
"vendor": "Vercel AI SDK",
|
|
481
|
+
"title": "AI SDK functions/classes removed in ai v6",
|
|
482
|
+
"severity": "medium",
|
|
483
|
+
"match": "pattern",
|
|
484
|
+
"applies_to": ["*"],
|
|
485
|
+
"status": "deprecated",
|
|
486
|
+
"detect": {
|
|
487
|
+
"sdk": ["ai"],
|
|
488
|
+
"patterns": [
|
|
489
|
+
"convertToCoreMessages",
|
|
490
|
+
"Experimental_Agent",
|
|
491
|
+
"MockLanguageModelV2",
|
|
492
|
+
"MockEmbeddingModelV2",
|
|
493
|
+
"MockImageModelV2",
|
|
494
|
+
"MockProviderV2",
|
|
495
|
+
"MockSpeechModelV2",
|
|
496
|
+
"MockTranscriptionModelV2"
|
|
497
|
+
],
|
|
498
|
+
"models": []
|
|
499
|
+
},
|
|
500
|
+
"migration_url": "https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0",
|
|
501
|
+
"summary": "Removed in ai v6. convertToCoreMessages → convertToModelMessages; Experimental_Agent → ToolLoopAgent; the V2 mock classes in ai/test → their V3 versions. Code using these breaks when you upgrade to ai@6.",
|
|
502
|
+
"source": "https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"id": "vercel-ai-sdk-v6-generateobject-deprecated",
|
|
506
|
+
"vendor": "Vercel AI SDK",
|
|
507
|
+
"title": "generateObject / streamObject deprecated in ai v6",
|
|
508
|
+
"severity": "low",
|
|
509
|
+
"match": "pattern",
|
|
510
|
+
"applies_to": ["*"],
|
|
511
|
+
"status": "deprecated",
|
|
512
|
+
"detect": {
|
|
513
|
+
"sdk": ["ai"],
|
|
514
|
+
"patterns": ["generateObject", "streamObject"],
|
|
515
|
+
"models": []
|
|
516
|
+
},
|
|
517
|
+
"migration_url": "https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0",
|
|
518
|
+
"summary": "Deprecated in ai v6 (still works; removal planned for a later version). Use generateText / streamText with an output setting (Output.object) instead.",
|
|
519
|
+
"source": "https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0"
|
|
477
520
|
}
|
|
478
521
|
]
|