model-eol 0.2.3 → 0.3.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/README.md +114 -26
- package/action.yml +124 -9
- package/bot/bot.mjs +978 -0
- package/bot/lib/common.mjs +120 -0
- package/bot/lib/config.mjs +1 -0
- package/bot/lib/eval.mjs +97 -0
- package/bot/lib/feeds.mjs +48 -0
- package/bot/lib/git.mjs +128 -0
- package/bot/lib/github.mjs +196 -0
- package/bot.yml.example +112 -0
- package/check.mjs +147 -42
- package/lib/config.mjs +133 -0
- package/lib/glob.mjs +35 -0
- package/lib/scanner.mjs +64 -5
- package/package.json +8 -4
- package/schema/model-eol.bot-config.schema.json +12 -8
package/lib/scanner.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
4
|
|
|
5
5
|
import { buildModelPattern } from './feeds.mjs'
|
|
6
|
+
import { matchesAnyGlob, matchesGlob, normalizeRepoPath } from './glob.mjs'
|
|
6
7
|
|
|
7
8
|
export const CODE_EXT = new Set([
|
|
8
9
|
'.c', '.cc', '.cjs', '.cpp', '.cs', '.cxx', '.go', '.h', '.hpp', '.java', '.js',
|
|
@@ -122,6 +123,7 @@ const classifyRef = ({ file, line, lines, publisher }) => {
|
|
|
122
123
|
|
|
123
124
|
const isScannable = (file, includeDocs) => {
|
|
124
125
|
const basename = path.basename(file)
|
|
126
|
+
if (basename === '.model-eol.json') return false
|
|
125
127
|
if (basename === '.env' || basename.startsWith('.env.')) return true
|
|
126
128
|
const ext = path.extname(file)
|
|
127
129
|
return CODE_EXT.has(ext) || (includeDocs && DOC_EXT.has(ext))
|
|
@@ -241,17 +243,58 @@ export const addedLinesForTargets = (targets, baseRef) => {
|
|
|
241
243
|
export const filterFindingsToChanged = (findings, addedLines) =>
|
|
242
244
|
findings.filter(finding => addedLines.has(changedKey(finding.file, finding.line)))
|
|
243
245
|
|
|
244
|
-
const collectFilesDetailed = (targets, {
|
|
246
|
+
const collectFilesDetailed = (targets, {
|
|
247
|
+
includeDocs = false,
|
|
248
|
+
ignorePaths = [],
|
|
249
|
+
ignoreRoots = [],
|
|
250
|
+
ignoredFiles = [],
|
|
251
|
+
} = {}) => {
|
|
245
252
|
const files = []
|
|
246
253
|
const notes = []
|
|
247
254
|
const validTargets = []
|
|
248
255
|
const seen = new Set()
|
|
256
|
+
const canonicalPath = value => {
|
|
257
|
+
const resolved = path.resolve(value)
|
|
258
|
+
try {
|
|
259
|
+
return fs.realpathSync(resolved)
|
|
260
|
+
} catch {
|
|
261
|
+
return resolved
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const excludedFiles = new Set(ignoredFiles.map(canonicalPath))
|
|
265
|
+
const roots = ignoreRoots.map(canonicalPath)
|
|
249
266
|
let fileLimitWarned = false
|
|
250
267
|
let stopped = false
|
|
251
268
|
|
|
252
269
|
const note = value => notes.push(value)
|
|
270
|
+
const repoPathsFor = absolute => {
|
|
271
|
+
const resolved = canonicalPath(absolute)
|
|
272
|
+
const repoPaths = []
|
|
273
|
+
for (const root of roots) {
|
|
274
|
+
const relative = path.relative(root, resolved)
|
|
275
|
+
if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue
|
|
276
|
+
repoPaths.push(normalizeRepoPath(relative))
|
|
277
|
+
}
|
|
278
|
+
return repoPaths
|
|
279
|
+
}
|
|
280
|
+
const matchesIgnoredRepoPath = repoPath => matchesAnyGlob(repoPath, ignorePaths) ||
|
|
281
|
+
ignorePaths.some(pattern => {
|
|
282
|
+
const normalizedPattern = normalizeRepoPath(pattern).replace(/\/+$/, '')
|
|
283
|
+
return normalizedPattern && !/[*?]/.test(normalizedPattern) && repoPath.startsWith(`${normalizedPattern}/`)
|
|
284
|
+
})
|
|
285
|
+
const isIgnoredFile = absolute => {
|
|
286
|
+
const resolved = canonicalPath(absolute)
|
|
287
|
+
return excludedFiles.has(resolved) || repoPathsFor(resolved).some(matchesIgnoredRepoPath)
|
|
288
|
+
}
|
|
289
|
+
const isIgnoredDirectoryTree = absolute => repoPathsFor(absolute).some(repoPath =>
|
|
290
|
+
ignorePaths.some(pattern => {
|
|
291
|
+
const normalizedPattern = normalizeRepoPath(pattern)
|
|
292
|
+
const literalDirectory = !/[*?]/.test(normalizedPattern) && normalizedPattern.replace(/\/+$/, '') === repoPath
|
|
293
|
+
const subtreeGlob = normalizedPattern.endsWith('/**') && matchesGlob(repoPath, normalizedPattern.slice(0, -3))
|
|
294
|
+
return literalDirectory || subtreeGlob
|
|
295
|
+
}))
|
|
253
296
|
const addFile = absolute => {
|
|
254
|
-
if (stopped || seen.has(absolute)) return
|
|
297
|
+
if (stopped || seen.has(absolute) || isIgnoredFile(absolute)) return
|
|
255
298
|
seen.add(absolute)
|
|
256
299
|
|
|
257
300
|
let st
|
|
@@ -288,7 +331,7 @@ const collectFilesDetailed = (targets, { includeDocs = false } = {}) => {
|
|
|
288
331
|
}
|
|
289
332
|
|
|
290
333
|
const walk = absolute => {
|
|
291
|
-
if (stopped) return
|
|
334
|
+
if (stopped || excludedFiles.has(canonicalPath(absolute))) return
|
|
292
335
|
let st
|
|
293
336
|
try {
|
|
294
337
|
st = fs.lstatSync(absolute)
|
|
@@ -301,6 +344,7 @@ const collectFilesDetailed = (targets, { includeDocs = false } = {}) => {
|
|
|
301
344
|
addFile(absolute)
|
|
302
345
|
return
|
|
303
346
|
}
|
|
347
|
+
if (isIgnoredDirectoryTree(absolute)) return
|
|
304
348
|
if (SKIP_DIRS.has(path.basename(absolute))) return
|
|
305
349
|
let names
|
|
306
350
|
try {
|
|
@@ -373,11 +417,24 @@ const collectFilesDetailed = (targets, { includeDocs = false } = {}) => {
|
|
|
373
417
|
|
|
374
418
|
export const collectFiles = (targets, options = {}) => collectFilesDetailed(targets, options).files
|
|
375
419
|
|
|
376
|
-
export const scanTargets = ({
|
|
377
|
-
|
|
420
|
+
export const scanTargets = ({
|
|
421
|
+
targets,
|
|
422
|
+
entries,
|
|
423
|
+
keys,
|
|
424
|
+
includeDocs = false,
|
|
425
|
+
ignoreModels = [],
|
|
426
|
+
ignorePaths = [],
|
|
427
|
+
ignoreRoots = [],
|
|
428
|
+
ignoredFiles = [],
|
|
429
|
+
}) => {
|
|
430
|
+
const collected = collectFilesDetailed(targets, { includeDocs, ignorePaths, ignoreRoots, ignoredFiles })
|
|
378
431
|
if (collected.validTargets.length === 0) throw new Error('no valid targets remain')
|
|
379
432
|
const { files } = collected
|
|
380
433
|
const pattern = buildModelPattern(keys)
|
|
434
|
+
const ignoredModelKeys = new Set(ignoreModels)
|
|
435
|
+
const ignoredCanonicalIds = new Set(ignoreModels.map(key => entries.get(key)?.entry.id ?? key))
|
|
436
|
+
const ignoresModel = (matched, id = null) =>
|
|
437
|
+
ignoredModelKeys.has(matched) || (id !== null && ignoredCanonicalIds.has(id))
|
|
381
438
|
const modelRefs = []
|
|
382
439
|
const candidateRefs = []
|
|
383
440
|
const integrationHints = []
|
|
@@ -424,6 +481,7 @@ export const scanTargets = ({ targets, entries, keys, includeDocs = false }) =>
|
|
|
424
481
|
let m
|
|
425
482
|
while ((m = pattern.exec(lineText)) !== null) {
|
|
426
483
|
const record = entries.get(m[1])
|
|
484
|
+
if (ignoresModel(m[1], record.entry.id)) continue
|
|
427
485
|
const occurrence = [...lineText.matchAll(new RegExp(escapeRegExp(m[1]), 'g'))]
|
|
428
486
|
.findIndex(match => match.index === m.index)
|
|
429
487
|
const classification = classifyRef({
|
|
@@ -453,6 +511,7 @@ export const scanTargets = ({ targets, entries, keys, includeDocs = false }) =>
|
|
|
453
511
|
}
|
|
454
512
|
|
|
455
513
|
for (const candidate of findCandidates(lineText, entries)) {
|
|
514
|
+
if (ignoresModel(candidate)) continue
|
|
456
515
|
const classification = classifyRef({
|
|
457
516
|
file,
|
|
458
517
|
line: i + 1,
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "model-eol",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AI model deprecation feeds with a zero-dependency checker, inventory CLI, and migration bot.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"model-eol": "check.mjs"
|
|
7
|
+
"model-eol": "check.mjs",
|
|
8
|
+
"model-eol-bot": "bot/bot.mjs"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"test": "node test/run.mjs && node refresh/test/run.mjs && node bot/test/run.mjs && node scripts/test-feed-changelog.mjs"
|
|
11
|
+
"test": "node test/run.mjs && node refresh/test/run.mjs && node bot/test/run.mjs && node scripts/test-action-contract.mjs && node bot/test/package.mjs && node scripts/test-feed-changelog.mjs"
|
|
11
12
|
},
|
|
12
13
|
"repository": {
|
|
13
14
|
"type": "git",
|
|
@@ -31,7 +32,10 @@
|
|
|
31
32
|
],
|
|
32
33
|
"files": [
|
|
33
34
|
"check.mjs",
|
|
35
|
+
"bot.yml.example",
|
|
34
36
|
"lib/",
|
|
37
|
+
"bot/bot.mjs",
|
|
38
|
+
"bot/lib/",
|
|
35
39
|
"feeds/",
|
|
36
40
|
"schema/",
|
|
37
41
|
"SPEC.md",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://example.invalid/model-eol/0.1/bot-config.schema.json",
|
|
4
|
-
"title": "model-eol
|
|
5
|
-
"description": "Optional .model-eol.json configuration
|
|
4
|
+
"title": "model-eol repository configuration",
|
|
5
|
+
"description": "Optional strict .model-eol.json configuration shared by the zero-dependency CLI, Action, and GitHub bot adapter.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
"scope": {
|
|
16
16
|
"enum": ["direct", "all"],
|
|
17
17
|
"default": "direct",
|
|
18
|
-
"description": "Use direct API references
|
|
18
|
+
"description": "Use the direct-first scope (direct API plus generic model references, leaving cloud/gateway references as hints), or include all tracked references."
|
|
19
19
|
},
|
|
20
20
|
"via": {
|
|
21
21
|
"type": ["string", "null"],
|
|
22
|
+
"minLength": 1,
|
|
22
23
|
"default": null,
|
|
23
24
|
"description": "Optional distributor lifecycle clock, such as azure-ai-foundry."
|
|
24
25
|
},
|
|
@@ -42,13 +43,13 @@
|
|
|
42
43
|
"type": "array",
|
|
43
44
|
"items": { "type": "string", "minLength": 1 },
|
|
44
45
|
"default": [],
|
|
45
|
-
"description": "Canonical IDs or aliases to ignore.
|
|
46
|
+
"description": "Canonical IDs or aliases to ignore across CLI, Action, and bot. Either form ignores every alias of the canonical feed entry."
|
|
46
47
|
},
|
|
47
48
|
"paths": {
|
|
48
49
|
"type": "array",
|
|
49
50
|
"items": { "type": "string", "minLength": 1 },
|
|
50
51
|
"default": [],
|
|
51
|
-
"description": "Repo-relative simple globs supporting *, **, and ?."
|
|
52
|
+
"description": "Repo-relative simple globs supporting *, **, and ?. Matching paths are excluded before file coverage checks where possible."
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
"default": { "models": [], "paths": [] }
|
|
@@ -71,17 +72,20 @@
|
|
|
71
72
|
"properties": {
|
|
72
73
|
"command": {
|
|
73
74
|
"type": ["string", "null"],
|
|
75
|
+
"minLength": 1,
|
|
74
76
|
"default": null,
|
|
75
|
-
"description": "Opt-in command
|
|
77
|
+
"description": "Opt-in evaluation command. Direct bot --eval runs it per changed patch in a temporary clone; the split example workflow runs it once as a read-only plan preflight."
|
|
76
78
|
},
|
|
77
79
|
"timeout_ms": {
|
|
78
80
|
"type": "integer",
|
|
79
|
-
"minimum":
|
|
81
|
+
"minimum": 1000,
|
|
82
|
+
"maximum": 3600000,
|
|
80
83
|
"default": 600000
|
|
81
84
|
},
|
|
82
85
|
"max_report_bytes": {
|
|
83
86
|
"type": "integer",
|
|
84
|
-
"minimum":
|
|
87
|
+
"minimum": 1,
|
|
88
|
+
"maximum": 8388608,
|
|
85
89
|
"default": 65536
|
|
86
90
|
},
|
|
87
91
|
"pass_env": {
|