model-eol 0.2.1
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/LICENSE +21 -0
- package/README.md +205 -0
- package/SPEC.md +134 -0
- package/action.yml +71 -0
- package/check.mjs +263 -0
- package/feeds/amazon.json +64 -0
- package/feeds/anthropic.json +252 -0
- package/feeds/google.json +542 -0
- package/feeds/openai.json +1158 -0
- package/lib/apply.mjs +409 -0
- package/lib/cli.mjs +25 -0
- package/lib/color.mjs +16 -0
- package/lib/feeds.mjs +169 -0
- package/lib/plan.mjs +159 -0
- package/lib/reports.mjs +322 -0
- package/lib/scanner.mjs +478 -0
- package/lib/validate-feed.mjs +246 -0
- package/package.json +45 -0
- package/schema/model-eol.alert.schema.json +28 -0
- package/schema/model-eol.bot-config.schema.json +106 -0
- package/schema/model-eol.inventory.schema.json +130 -0
- package/schema/model-eol.plan.schema.json +82 -0
- package/schema/model-eol.schedule.schema.json +31 -0
- package/schema/model-eol.schema.json +69 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
2
|
+
export const MAX_IDENTIFIER_LENGTH = 256
|
|
3
|
+
export const MAX_TEXT_LENGTH = 4096
|
|
4
|
+
export const MAX_URL_LENGTH = 4096
|
|
5
|
+
export const MAX_REPORT_BYTES = 8 * 1024 * 1024
|
|
6
|
+
export const MIN_TIMEOUT_MS = 1000
|
|
7
|
+
export const MAX_TIMEOUT_MS = 3600000
|
|
8
|
+
|
|
9
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/
|
|
10
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
11
|
+
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
|
|
12
|
+
const DATE_PRECISIONS = new Set(['exact', 'earliest'])
|
|
13
|
+
const DISTRIBUTION_STATUSES = new Set(['active', 'legacy', 'extended-access', 'retired'])
|
|
14
|
+
|
|
15
|
+
export const assertIsoDate = (value, label = 'date') => {
|
|
16
|
+
if (typeof value !== 'string' || !DATE_PATTERN.test(value)) {
|
|
17
|
+
throw new Error(`${label} must be YYYY-MM-DD`)
|
|
18
|
+
}
|
|
19
|
+
const [year, month, day] = value.split('-').map(Number)
|
|
20
|
+
const date = new Date(Date.UTC(year, month - 1, day))
|
|
21
|
+
if (year >= 0 && year <= 99) date.setUTCFullYear(year)
|
|
22
|
+
if (
|
|
23
|
+
date.getUTCFullYear() !== year ||
|
|
24
|
+
date.getUTCMonth() + 1 !== month ||
|
|
25
|
+
date.getUTCDate() !== day
|
|
26
|
+
) throw new Error(`${label} must be a real calendar date`)
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isObject = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
31
|
+
|
|
32
|
+
const isUrl = value => {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(value)
|
|
35
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const validateFeed = feed => {
|
|
42
|
+
const errors = []
|
|
43
|
+
const fail = (path, message) => errors.push({ path, message })
|
|
44
|
+
const checkString = (value, path, limit = MAX_TEXT_LENGTH) => {
|
|
45
|
+
if (typeof value !== 'string') return false
|
|
46
|
+
if (CONTROL_CHARACTER_PATTERN.test(value)) fail(path, 'must not contain control characters')
|
|
47
|
+
if (value.length > limit) fail(path, `must be at most ${limit} characters`)
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
const checkIdentifier = (value, path, required = false) => {
|
|
51
|
+
if (value === undefined && !required) return false
|
|
52
|
+
if (!checkString(value, path, MAX_IDENTIFIER_LENGTH)) {
|
|
53
|
+
fail(path, required ? 'must be a string' : 'must be a string')
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
if (!value || !MODEL_ID_PATTERN.test(value)) fail(path, 'must be a grammar-valid model ID')
|
|
57
|
+
return Boolean(value && MODEL_ID_PATTERN.test(value))
|
|
58
|
+
}
|
|
59
|
+
const checkUrl = (value, path) => {
|
|
60
|
+
if (value === undefined) return false
|
|
61
|
+
if (!checkString(value, path, MAX_URL_LENGTH)) {
|
|
62
|
+
fail(path, 'must be a string URL')
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
if (!isUrl(value)) fail(path, 'must be an http or https URL')
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
const checkDate = (value, path) => {
|
|
69
|
+
if (value === undefined) return false
|
|
70
|
+
if (!checkString(value, path, 32)) {
|
|
71
|
+
fail(path, 'must be YYYY-MM-DD')
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
assertIsoDate(value, path)
|
|
76
|
+
return true
|
|
77
|
+
} catch (error) {
|
|
78
|
+
fail(path, error.message)
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const checkDateTime = (value, path) => {
|
|
83
|
+
if (!checkString(value, path, 64)) {
|
|
84
|
+
fail(path, 'must be an ISO date-time')
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
if (!DATE_TIME_PATTERN.test(value) || Number.isNaN(Date.parse(value))) {
|
|
88
|
+
fail(path, 'must be an ISO date-time')
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
assertIsoDate(value.slice(0, 10), path)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
fail(path, error.message)
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
const checkObjectKeys = (value, path, allowed) => {
|
|
100
|
+
if (!isObject(value)) {
|
|
101
|
+
fail(path, 'must be an object')
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
for (const key of Object.keys(value)) {
|
|
105
|
+
if (!allowed.has(key)) fail(`${path}.${key}`, 'unknown field')
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
const walkStrings = (value, path, seen = new Set()) => {
|
|
110
|
+
if (typeof value === 'string') {
|
|
111
|
+
checkString(value, path)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return
|
|
115
|
+
seen.add(value)
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
value.forEach((item, index) => walkStrings(item, `${path}[${index}]`, seen))
|
|
118
|
+
} else {
|
|
119
|
+
for (const [key, item] of Object.entries(value)) walkStrings(item, `${path}.${key}`, seen)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
walkStrings(feed, 'feed')
|
|
124
|
+
if (!isObject(feed)) {
|
|
125
|
+
fail('feed', 'must be a JSON object')
|
|
126
|
+
return errors
|
|
127
|
+
}
|
|
128
|
+
checkObjectKeys(feed, 'feed', new Set(['spec', 'publisher', 'generated', 'source', 'note', 'policy', 'models']))
|
|
129
|
+
if (feed.spec !== 'model-eol/0.1') fail('spec', `must be model-eol/0.1, got ${feed.spec}`)
|
|
130
|
+
if (!checkString(feed.publisher, 'publisher', MAX_IDENTIFIER_LENGTH) || !feed.publisher) fail('publisher', 'must be a non-empty string')
|
|
131
|
+
checkDateTime(feed.generated, 'generated')
|
|
132
|
+
checkUrl(feed.source, 'source')
|
|
133
|
+
if (feed.note !== undefined) checkString(feed.note, 'note', MAX_TEXT_LENGTH)
|
|
134
|
+
if (!Array.isArray(feed.models)) {
|
|
135
|
+
fail('models', 'must be an array')
|
|
136
|
+
return errors
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (feed.policy !== undefined && checkObjectKeys(feed.policy, 'policy', new Set(['min_notice_days', 'source']))) {
|
|
140
|
+
if (!Number.isSafeInteger(feed.policy.min_notice_days) || feed.policy.min_notice_days < 0) {
|
|
141
|
+
fail('policy.min_notice_days', 'must be a non-negative safe integer')
|
|
142
|
+
}
|
|
143
|
+
if (feed.policy.source === undefined) fail('policy.source', 'is required')
|
|
144
|
+
else checkUrl(feed.policy.source, 'policy.source')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const modelKeys = new Set()
|
|
148
|
+
const seenKeys = new Map()
|
|
149
|
+
const validDates = []
|
|
150
|
+
for (const [index, model] of feed.models.entries()) {
|
|
151
|
+
const at = `models[${index}]`
|
|
152
|
+
if (!checkObjectKeys(model, at, new Set([
|
|
153
|
+
'id', 'aliases', 'announced', 'shutdown', 'date_precision', 'replacement',
|
|
154
|
+
'replacement_options', 'replacement_note', 'notes', 'source', 'distributions',
|
|
155
|
+
]))) continue
|
|
156
|
+
|
|
157
|
+
const idValid = checkIdentifier(model.id, `${at}.id`, true)
|
|
158
|
+
if (idValid) {
|
|
159
|
+
modelKeys.add(model.id)
|
|
160
|
+
const previous = seenKeys.get(model.id)
|
|
161
|
+
if (previous) fail(`${at}.id`, `duplicate id/alias "${model.id}" also used at ${previous}`)
|
|
162
|
+
else seenKeys.set(model.id, `${at}.id`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (model.aliases !== undefined) {
|
|
166
|
+
if (!Array.isArray(model.aliases) || model.aliases.length === 0) {
|
|
167
|
+
fail(`${at}.aliases`, 'must be a non-empty array')
|
|
168
|
+
} else {
|
|
169
|
+
for (const [aliasIndex, alias] of model.aliases.entries()) {
|
|
170
|
+
const aliasPath = `${at}.aliases[${aliasIndex}]`
|
|
171
|
+
if (!checkIdentifier(alias, aliasPath, true)) continue
|
|
172
|
+
modelKeys.add(alias)
|
|
173
|
+
const previous = seenKeys.get(alias)
|
|
174
|
+
if (previous) fail(aliasPath, `duplicate id/alias "${alias}" also used at ${previous}`)
|
|
175
|
+
else seenKeys.set(alias, aliasPath)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const announcedValid = checkDate(model.announced, `${at}.announced`)
|
|
181
|
+
const shutdownValid = checkDate(model.shutdown, `${at}.shutdown`)
|
|
182
|
+
if (announcedValid && shutdownValid) validDates.push({ at, announced: model.announced, shutdown: model.shutdown })
|
|
183
|
+
if (model.date_precision !== undefined) {
|
|
184
|
+
if (typeof model.date_precision !== 'string' || !DATE_PRECISIONS.has(model.date_precision)) fail(`${at}.date_precision`, 'must be exact or earliest')
|
|
185
|
+
}
|
|
186
|
+
if (model.replacement !== undefined) checkIdentifier(model.replacement, `${at}.replacement`)
|
|
187
|
+
if (model.replacement_options !== undefined) {
|
|
188
|
+
if (!Array.isArray(model.replacement_options) || model.replacement_options.length === 0) {
|
|
189
|
+
fail(`${at}.replacement_options`, 'must be a non-empty array')
|
|
190
|
+
} else {
|
|
191
|
+
for (const [optionIndex, option] of model.replacement_options.entries()) {
|
|
192
|
+
checkIdentifier(option, `${at}.replacement_options[${optionIndex}]`, true)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (model.replacement !== undefined) fail(at, 'replacement and replacement_options are mutually exclusive')
|
|
196
|
+
}
|
|
197
|
+
if (model.replacement_note !== undefined) checkString(model.replacement_note, `${at}.replacement_note`, MAX_TEXT_LENGTH)
|
|
198
|
+
if (model.notes !== undefined) checkString(model.notes, `${at}.notes`, MAX_TEXT_LENGTH)
|
|
199
|
+
checkUrl(model.source, `${at}.source`)
|
|
200
|
+
if (model.announced || model.shutdown) {
|
|
201
|
+
if (!(model.source || feed.source)) fail(at, 'dated entry needs a source (entry- or feed-level)')
|
|
202
|
+
}
|
|
203
|
+
if (model.distributions !== undefined) {
|
|
204
|
+
if (!Array.isArray(model.distributions)) {
|
|
205
|
+
fail(`${at}.distributions`, 'must be an array')
|
|
206
|
+
} else {
|
|
207
|
+
for (const [distributionIndex, distribution] of model.distributions.entries()) {
|
|
208
|
+
const dAt = `${at}.distributions[${distributionIndex}]`
|
|
209
|
+
if (!checkObjectKeys(distribution, dAt, new Set(['via', 'announced', 'shutdown', 'date_precision', 'status', 'source']))) continue
|
|
210
|
+
if (!checkString(distribution.via, `${dAt}.via`, MAX_IDENTIFIER_LENGTH) || !distribution.via) fail(`${dAt}.via`, 'must be a non-empty string')
|
|
211
|
+
const distributionAnnounced = checkDate(distribution.announced, `${dAt}.announced`)
|
|
212
|
+
const distributionShutdown = checkDate(distribution.shutdown, `${dAt}.shutdown`)
|
|
213
|
+
if (distributionAnnounced && distributionShutdown && distribution.shutdown < distribution.announced) fail(dAt, 'shutdown precedes announced')
|
|
214
|
+
if (distribution.date_precision !== undefined && (typeof distribution.date_precision !== 'string' || !DATE_PRECISIONS.has(distribution.date_precision))) fail(`${dAt}.date_precision`, 'must be exact or earliest')
|
|
215
|
+
if (distribution.status !== undefined && (typeof distribution.status !== 'string' || !DISTRIBUTION_STATUSES.has(distribution.status))) fail(`${dAt}.status`, 'has an unsupported status')
|
|
216
|
+
checkUrl(distribution.source, `${dAt}.source`)
|
|
217
|
+
if (distribution.shutdown && !(distribution.source || model.source || feed.source)) fail(dAt, 'dated distribution needs a source')
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const date of validDates) {
|
|
224
|
+
if (date.shutdown < date.announced) fail(date.at, 'shutdown precedes announced')
|
|
225
|
+
}
|
|
226
|
+
for (const [index, model] of feed.models.entries()) {
|
|
227
|
+
if (!isObject(model) || typeof model.replacement !== 'string' || !MODEL_ID_PATTERN.test(model.replacement)) continue
|
|
228
|
+
if (!modelKeys.has(model.replacement)) fail(`models[${index}].replacement`, `does not resolve to an id or alias in this feed: ${model.replacement}`)
|
|
229
|
+
}
|
|
230
|
+
return errors
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const assertValidFeed = (feed, label = 'feed') => {
|
|
234
|
+
const errors = validateFeed(feed)
|
|
235
|
+
if (errors.length) {
|
|
236
|
+
const models = Array.isArray(feed?.models) ? feed.models : []
|
|
237
|
+
const lines = errors.map(error => {
|
|
238
|
+
const modelIndex = error.path.match(/^models\[(\d+)\]/)?.[1]
|
|
239
|
+
const modelId = modelIndex !== undefined ? models[Number(modelIndex)]?.id : models.find(model => model?.id)?.id
|
|
240
|
+
const context = typeof modelId === 'string' ? ` model ${modelId}` : ''
|
|
241
|
+
return `${label}${context}: ${error.path}: ${error.message}`
|
|
242
|
+
})
|
|
243
|
+
throw new Error(lines.join('\n'))
|
|
244
|
+
}
|
|
245
|
+
return feed
|
|
246
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "model-eol",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "A machine-readable deprecation feed format for AI models, plus a zero-dependency checker and inventory CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"model-eol": "check.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test/run.mjs && node refresh/test/run.mjs && node bot/test/run.mjs && node scripts/test-feed-changelog.mjs"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/thossullivan/model-eol.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/thossullivan/model-eol/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/thossullivan/model-eol",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Tom Sullivan",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"model deprecation",
|
|
24
|
+
"eol",
|
|
25
|
+
"llm",
|
|
26
|
+
"dependabot",
|
|
27
|
+
"sbom",
|
|
28
|
+
"model lifecycle",
|
|
29
|
+
"ai models",
|
|
30
|
+
"software bill of materials"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"check.mjs",
|
|
34
|
+
"lib/",
|
|
35
|
+
"feeds/",
|
|
36
|
+
"schema/",
|
|
37
|
+
"SPEC.md",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"action.yml"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://example.invalid/model-eol/0.1/alert.schema.json",
|
|
4
|
+
"title": "model-eol alert output",
|
|
5
|
+
"description": "Machine-readable alert payload emitted by `model-eol alert --json`. Draft 0.1 - the $id is a placeholder until the repo is public.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema", "generated", "threshold_days", "scope", "scan_notes", "errors", "warnings"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema": { "const": "model-eol/alert@0.1" },
|
|
10
|
+
"generated": { "type": "string", "format": "date-time" },
|
|
11
|
+
"threshold_days": { "type": "number" },
|
|
12
|
+
"distributor": { "type": ["string", "null"] },
|
|
13
|
+
"scope": { "enum": ["all", "direct"] },
|
|
14
|
+
"scan_notes": { "type": "array", "items": { "$ref": "model-eol.inventory.schema.json#/definitions/scanNote" } },
|
|
15
|
+
"errors": { "type": "array", "items": { "$ref": "model-eol.inventory.schema.json#/definitions/modelReference" } },
|
|
16
|
+
"warnings": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"oneOf": [
|
|
20
|
+
{ "$ref": "model-eol.inventory.schema.json#/definitions/modelReference" },
|
|
21
|
+
{ "$ref": "model-eol.inventory.schema.json#/definitions/candidateReference" },
|
|
22
|
+
{ "$ref": "model-eol.inventory.schema.json#/definitions/integrationHint" }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"additionalProperties": false
|
|
28
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://example.invalid/model-eol/0.1/bot-config.schema.json",
|
|
4
|
+
"title": "model-eol bot configuration",
|
|
5
|
+
"description": "Optional .model-eol.json configuration for the zero-dependency GitHub bot adapter.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"days": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"minimum": 0,
|
|
12
|
+
"default": 90,
|
|
13
|
+
"description": "Open migration work for models retiring within this many days."
|
|
14
|
+
},
|
|
15
|
+
"scope": {
|
|
16
|
+
"enum": ["direct", "all"],
|
|
17
|
+
"default": "direct",
|
|
18
|
+
"description": "Use direct API references only, or include generic model references."
|
|
19
|
+
},
|
|
20
|
+
"via": {
|
|
21
|
+
"type": ["string", "null"],
|
|
22
|
+
"default": null,
|
|
23
|
+
"description": "Optional distributor lifecycle clock, such as azure-ai-foundry."
|
|
24
|
+
},
|
|
25
|
+
"feeds": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"allow_vendored_fallback": {
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"default": false,
|
|
32
|
+
"description": "Continue with vendored feeds after a remote feed failure in degraded report-only mode."
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"default": { "allow_vendored_fallback": false }
|
|
36
|
+
},
|
|
37
|
+
"ignore": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"properties": {
|
|
41
|
+
"models": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": { "type": "string", "minLength": 1 },
|
|
44
|
+
"default": [],
|
|
45
|
+
"description": "Canonical IDs or aliases to ignore. A canonical ID ignores all of its aliases."
|
|
46
|
+
},
|
|
47
|
+
"paths": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": { "type": "string", "minLength": 1 },
|
|
50
|
+
"default": [],
|
|
51
|
+
"description": "Repo-relative simple globs supporting *, **, and ?."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"default": { "models": [], "paths": [] }
|
|
55
|
+
},
|
|
56
|
+
"issues": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"properties": {
|
|
60
|
+
"enabled": {
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"default": true,
|
|
63
|
+
"description": "Maintain GitHub issues for non-patchable actionable findings."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"default": { "enabled": true }
|
|
67
|
+
},
|
|
68
|
+
"eval": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"properties": {
|
|
72
|
+
"command": {
|
|
73
|
+
"type": ["string", "null"],
|
|
74
|
+
"default": null,
|
|
75
|
+
"description": "Opt-in command run in the temporary clone for changed patches."
|
|
76
|
+
},
|
|
77
|
+
"timeout_ms": {
|
|
78
|
+
"type": "integer",
|
|
79
|
+
"minimum": 1,
|
|
80
|
+
"default": 600000
|
|
81
|
+
},
|
|
82
|
+
"max_report_bytes": {
|
|
83
|
+
"type": "integer",
|
|
84
|
+
"minimum": 0,
|
|
85
|
+
"default": 65536
|
|
86
|
+
},
|
|
87
|
+
"pass_env": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": { "type": "string", "minLength": 1 },
|
|
90
|
+
"default": [],
|
|
91
|
+
"description": "Environment variable names explicitly passed to the eval command."
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"default": { "command": null, "timeout_ms": 600000, "max_report_bytes": 65536, "pass_env": [] }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"default": {
|
|
98
|
+
"days": 90,
|
|
99
|
+
"scope": "direct",
|
|
100
|
+
"via": null,
|
|
101
|
+
"feeds": { "allow_vendored_fallback": false },
|
|
102
|
+
"ignore": { "models": [], "paths": [] },
|
|
103
|
+
"issues": { "enabled": true },
|
|
104
|
+
"eval": { "command": null, "timeout_ms": 600000, "max_report_bytes": 65536, "pass_env": [] }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://example.invalid/model-eol/0.1/inventory.schema.json",
|
|
4
|
+
"title": "model-eol inventory output",
|
|
5
|
+
"description": "Machine-readable repository inventory emitted by `model-eol inventory --json`. Draft 0.1 - the $id is a placeholder until the repo is public.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema", "generated", "threshold_days", "scope", "targets", "scanned_files", "scan_notes", "summary", "model_references", "candidate_model_references", "integration_hints"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema": { "const": "model-eol/inventory@0.1" },
|
|
10
|
+
"generated": { "type": "string", "format": "date-time" },
|
|
11
|
+
"threshold_days": { "type": "number" },
|
|
12
|
+
"distributor": { "type": ["string", "null"] },
|
|
13
|
+
"scope": { "enum": ["all", "direct"] },
|
|
14
|
+
"targets": { "type": "array", "items": { "type": "string" } },
|
|
15
|
+
"scanned_files": { "type": "integer", "minimum": 0 },
|
|
16
|
+
"scan_notes": { "type": "array", "items": { "$ref": "#/definitions/scanNote" } },
|
|
17
|
+
"summary": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["model_references", "candidate_model_references", "integration_hints", "retired_or_retiring", "cloud_or_gateway_hints"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"model_references": { "type": "integer", "minimum": 0 },
|
|
22
|
+
"candidate_model_references": { "type": "integer", "minimum": 0 },
|
|
23
|
+
"integration_hints": { "type": "integer", "minimum": 0 },
|
|
24
|
+
"retired_or_retiring": { "type": "integer", "minimum": 0 },
|
|
25
|
+
"cloud_or_gateway_hints": { "type": "integer", "minimum": 0 }
|
|
26
|
+
},
|
|
27
|
+
"additionalProperties": false
|
|
28
|
+
},
|
|
29
|
+
"model_references": { "type": "array", "items": { "$ref": "#/definitions/modelReference" } },
|
|
30
|
+
"candidate_model_references": { "type": "array", "items": { "$ref": "#/definitions/candidateReference" } },
|
|
31
|
+
"integration_hints": { "type": "array", "items": { "$ref": "#/definitions/integrationHint" } }
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"definitions": {
|
|
35
|
+
"usage": { "enum": ["direct-api", "cloud-provider", "gateway", "model-reference"] },
|
|
36
|
+
"confidence": { "enum": ["high", "medium", "low"] },
|
|
37
|
+
"status": { "enum": ["retired", "retiring", "scheduled", "watch", "ok"] },
|
|
38
|
+
"location": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["file", "line"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"file": { "type": "string" },
|
|
43
|
+
"line": { "type": "integer", "minimum": 1 }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"modelReference": {
|
|
47
|
+
"allOf": [
|
|
48
|
+
{ "$ref": "#/definitions/location" },
|
|
49
|
+
{
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["kind", "matched", "id", "publisher", "usage", "resolved_provider", "confidence", "evidence", "status", "shutdown", "via", "days", "safe_until", "replacement", "source"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"kind": { "const": "model-reference" },
|
|
54
|
+
"matched": { "type": "string" },
|
|
55
|
+
"id": { "type": "string" },
|
|
56
|
+
"publisher": { "type": "string" },
|
|
57
|
+
"usage": { "$ref": "#/definitions/usage" },
|
|
58
|
+
"resolved_provider": { "type": "string" },
|
|
59
|
+
"confidence": { "$ref": "#/definitions/confidence" },
|
|
60
|
+
"evidence": { "type": "string" },
|
|
61
|
+
"status": { "$ref": "#/definitions/status" },
|
|
62
|
+
"shutdown": { "type": ["string", "null"] },
|
|
63
|
+
"via": { "type": ["string", "null"] },
|
|
64
|
+
"days": { "type": ["integer", "null"] },
|
|
65
|
+
"safe_until": { "type": ["string", "null"], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
66
|
+
"replacement": { "type": ["string", "null"] },
|
|
67
|
+
"source": { "type": ["string", "null"] },
|
|
68
|
+
"policy": {
|
|
69
|
+
"type": ["object", "null"],
|
|
70
|
+
"required": ["min_notice_days", "source"],
|
|
71
|
+
"properties": {
|
|
72
|
+
"min_notice_days": { "type": "integer", "minimum": 0 },
|
|
73
|
+
"source": { "type": "string", "format": "uri" }
|
|
74
|
+
},
|
|
75
|
+
"additionalProperties": false
|
|
76
|
+
},
|
|
77
|
+
"feed_generated": { "type": ["string", "null"], "format": "date-time" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"candidateReference": {
|
|
83
|
+
"allOf": [
|
|
84
|
+
{ "$ref": "#/definitions/location" },
|
|
85
|
+
{
|
|
86
|
+
"type": "object",
|
|
87
|
+
"required": ["kind", "matched", "usage", "resolved_provider", "confidence", "evidence", "reason"],
|
|
88
|
+
"properties": {
|
|
89
|
+
"kind": { "const": "candidate-model-reference" },
|
|
90
|
+
"matched": { "type": "string" },
|
|
91
|
+
"usage": { "$ref": "#/definitions/usage" },
|
|
92
|
+
"resolved_provider": { "type": "string" },
|
|
93
|
+
"confidence": { "$ref": "#/definitions/confidence" },
|
|
94
|
+
"evidence": { "type": "string" },
|
|
95
|
+
"reason": { "type": "string" }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"integrationHint": {
|
|
101
|
+
"allOf": [
|
|
102
|
+
{ "$ref": "#/definitions/location" },
|
|
103
|
+
{
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": ["kind", "usage", "provider", "matched", "evidence"],
|
|
106
|
+
"properties": {
|
|
107
|
+
"kind": { "const": "integration-hint" },
|
|
108
|
+
"usage": { "enum": ["direct-api", "cloud-provider", "gateway"] },
|
|
109
|
+
"provider": { "type": "string" },
|
|
110
|
+
"matched": { "type": "string" },
|
|
111
|
+
"evidence": { "type": "string" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"scanNote": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["reason"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
121
|
+
"file": { "type": "string" },
|
|
122
|
+
"message": { "type": "string" },
|
|
123
|
+
"bytes": { "type": "integer", "minimum": 0 },
|
|
124
|
+
"limit_bytes": { "type": "integer", "minimum": 0 },
|
|
125
|
+
"limit_files": { "type": "integer", "minimum": 0 }
|
|
126
|
+
},
|
|
127
|
+
"additionalProperties": false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://example.invalid/model-eol/0.1/plan.schema.json",
|
|
4
|
+
"title": "model-eol migration plan",
|
|
5
|
+
"description": "Deterministic, hash-guarded migration plan emitted by `model-eol plan`. Draft 0.1 - the $id is a placeholder until the repo is public.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["plan_schema", "generated", "threshold_days", "via", "scan_notes", "items", "issues"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"plan_schema": { "const": "model-eol.plan/0.1" },
|
|
10
|
+
"generated": { "type": "string", "format": "date-time" },
|
|
11
|
+
"threshold_days": { "type": "number" },
|
|
12
|
+
"via": { "type": ["string", "null"] },
|
|
13
|
+
"scan_notes": { "type": "array", "items": { "$ref": "#/definitions/scanNote" } },
|
|
14
|
+
"items": { "type": "array", "items": { "$ref": "#/definitions/item" } },
|
|
15
|
+
"issues": { "type": "array", "items": { "$ref": "#/definitions/issue" } }
|
|
16
|
+
},
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"definitions": {
|
|
19
|
+
"item": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["file", "line", "occurrence", "matched", "expected_line_sha256", "id", "publisher", "replacement", "shutdown", "days", "status", "sources", "notes"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"file": { "type": "string", "minLength": 1 },
|
|
24
|
+
"line": { "type": "integer", "minimum": 1 },
|
|
25
|
+
"occurrence": { "type": "integer", "minimum": 0 },
|
|
26
|
+
"matched": { "type": "string", "minLength": 1 },
|
|
27
|
+
"expected_line_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
28
|
+
"id": { "type": "string", "minLength": 1 },
|
|
29
|
+
"publisher": { "type": "string", "minLength": 1 },
|
|
30
|
+
"replacement": { "type": "string", "minLength": 1 },
|
|
31
|
+
"shutdown": { "type": "string" },
|
|
32
|
+
"days": { "type": "integer" },
|
|
33
|
+
"status": { "enum": ["retired", "retiring"] },
|
|
34
|
+
"sources": { "type": "array", "items": { "type": "string" } },
|
|
35
|
+
"notes": { "type": ["string", "null"] }
|
|
36
|
+
},
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
},
|
|
39
|
+
"issue": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["file", "line", "reason"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"file": { "type": "string" },
|
|
44
|
+
"line": { "type": "integer", "minimum": 1 },
|
|
45
|
+
"matched": { "type": "string" },
|
|
46
|
+
"id": { "type": "string" },
|
|
47
|
+
"publisher": { "type": "string" },
|
|
48
|
+
"usage": { "enum": ["direct-api", "cloud-provider", "gateway", "model-reference"] },
|
|
49
|
+
"confidence": { "enum": ["high", "medium", "low"] },
|
|
50
|
+
"status": { "enum": ["retired", "retiring", "scheduled", "watch", "ok"] },
|
|
51
|
+
"shutdown": { "type": ["string", "null"] },
|
|
52
|
+
"via": { "type": ["string", "null"] },
|
|
53
|
+
"days": { "type": ["integer", "null"] },
|
|
54
|
+
"replacement": { "type": ["string", "null"] },
|
|
55
|
+
"replacement_options": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"minItems": 1,
|
|
58
|
+
"items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" }
|
|
59
|
+
},
|
|
60
|
+
"replacement_note": { "type": "string" },
|
|
61
|
+
"provider": { "type": "string" },
|
|
62
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
63
|
+
"sources": { "type": "array", "items": { "type": "string" } },
|
|
64
|
+
"notes": { "type": ["string", "null"] }
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
},
|
|
68
|
+
"scanNote": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"required": ["reason"],
|
|
71
|
+
"properties": {
|
|
72
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
73
|
+
"file": { "type": "string" },
|
|
74
|
+
"message": { "type": "string" },
|
|
75
|
+
"bytes": { "type": "integer", "minimum": 0 },
|
|
76
|
+
"limit_bytes": { "type": "integer", "minimum": 0 },
|
|
77
|
+
"limit_files": { "type": "integer", "minimum": 0 }
|
|
78
|
+
},
|
|
79
|
+
"additionalProperties": false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|