model-eol 0.5.2 → 0.6.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/README.md +344 -230
- package/SPEC.md +12 -1
- package/bot/bot.mjs +19 -6
- package/bot/lib/common.mjs +6 -0
- package/feeds/anthropic.json +47 -11
- package/feeds/google.json +26 -3
- package/feeds/openai.json +4 -4
- package/lib/config.mjs +61 -3
- package/lib/feeds.mjs +36 -6
- package/lib/plan.mjs +7 -0
- package/lib/reports.mjs +80 -16
- package/lib/scanner.mjs +2 -0
- package/lib/validate-document.mjs +1 -1
- package/lib/validate-feed.mjs +22 -3
- package/package.json +1 -1
- package/schema/model-eol.bot-config.schema.json +28 -1
- package/schema/model-eol.check.schema.json +2 -1
- package/schema/model-eol.inventory.schema.json +13 -1
- package/schema/model-eol.plan.schema.json +13 -0
- package/schema/model-eol.schema.json +2 -2
package/lib/reports.mjs
CHANGED
|
@@ -8,9 +8,16 @@ export const marker = status =>
|
|
|
8
8
|
status === 'watch' ? '?' :
|
|
9
9
|
'·'
|
|
10
10
|
|
|
11
|
-
const shutdownText = finding =>
|
|
12
|
-
? `
|
|
13
|
-
|
|
11
|
+
const shutdownText = finding =>
|
|
12
|
+
finding.date_precision === 'tentative' ? `not sooner than ${finding.shutdown}` :
|
|
13
|
+
finding.date_precision === 'earliest' ? `no earlier than ${finding.shutdown}` :
|
|
14
|
+
finding.shutdown
|
|
15
|
+
|
|
16
|
+
const tentativeFloor = finding => finding.date_precision === 'tentative'
|
|
17
|
+
|
|
18
|
+
const policyTail = finding => finding.safe_until && finding.shutdown && finding.safe_until > finding.shutdown
|
|
19
|
+
? `, guaranteed until ${finding.safe_until} by ${finding.publisher} policy`
|
|
20
|
+
: ''
|
|
14
21
|
|
|
15
22
|
const replacementTail = finding => finding.replacement
|
|
16
23
|
? ` -> ${finding.replacement}`
|
|
@@ -18,22 +25,32 @@ const replacementTail = finding => finding.replacement
|
|
|
18
25
|
? ` options: ${finding.replacement_options.join(' | ')}`
|
|
19
26
|
: ''
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
const lifecycleTail = (f, days) =>
|
|
22
29
|
f.status === 'retired' && !f.shutdown ? `RETIRED (shutdown date unavailable)${replacementTail(f)}` :
|
|
23
30
|
f.status === 'retired' ? `RETIRED ${shutdownText(f)} (${-f.days} days ago)${replacementTail(f)}` :
|
|
24
31
|
f.status === 'retiring' ? `RETIRES ${shutdownText(f)} (${f.days} days)${replacementTail(f)}` :
|
|
32
|
+
tentativeFloor(f) ? `scheduled - tentative, not announced: ${shutdownText(f)}${policyTail(f)}` :
|
|
25
33
|
f.status === 'scheduled' ? `scheduled ${shutdownText(f)} (${f.days} days, outside --days ${f.threshold_days ?? days})` :
|
|
26
34
|
f.status === 'watch' ? 'deprecation announced, no shutdown date yet' :
|
|
27
35
|
'no retirement scheduled'
|
|
28
36
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const waiverTail = finding => !finding.waiver
|
|
38
|
+
? ''
|
|
39
|
+
: finding.waiver.active
|
|
40
|
+
? ` [waived until ${finding.waiver.expires} by ${finding.waiver.owner}: ${finding.waiver.reason}]`
|
|
41
|
+
: ` [waiver expired on ${finding.waiver.expires}; owner ${finding.waiver.owner}: ${finding.waiver.reason}]`
|
|
42
|
+
|
|
43
|
+
export const findingTail = (finding, days) => `${lifecycleTail(finding, days)}${waiverTail(finding)}`
|
|
44
|
+
|
|
45
|
+
const statusStyle = finding =>
|
|
46
|
+
finding.waiver?.active === true ? 'yellow' :
|
|
47
|
+
finding.status === 'retired' ? 'red' :
|
|
48
|
+
finding.status === 'retiring' ? 'yellow' :
|
|
49
|
+
finding.status === 'ok' ? 'dim' :
|
|
33
50
|
null
|
|
34
51
|
|
|
35
|
-
const humanFinding = (
|
|
36
|
-
const style = statusStyle(
|
|
52
|
+
const humanFinding = (finding, line) => {
|
|
53
|
+
const style = statusStyle(finding)
|
|
37
54
|
return style ? color(style, line) : line
|
|
38
55
|
}
|
|
39
56
|
|
|
@@ -67,6 +84,7 @@ export const buildInventory = ({ scan, findings, days, via, scope, targets }) =>
|
|
|
67
84
|
replacement: lifecycle.replacement,
|
|
68
85
|
...(lifecycle.replacement_options?.length ? { replacement_options: lifecycle.replacement_options } : {}),
|
|
69
86
|
...(lifecycle.replacement_note ? { replacement_note: lifecycle.replacement_note } : {}),
|
|
87
|
+
...(lifecycle.waiver ? { waiver: lifecycle.waiver } : {}),
|
|
70
88
|
source: ref.source,
|
|
71
89
|
policy: ref.policy ?? null,
|
|
72
90
|
feed_generated: ref.generated ?? null,
|
|
@@ -149,9 +167,11 @@ export const buildSchedule = inventory => {
|
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
export const buildAlert = schedule => {
|
|
152
|
-
const errors = schedule.items.filter(
|
|
170
|
+
const errors = schedule.items.filter(isBad)
|
|
153
171
|
const warnings = [
|
|
154
|
-
...schedule.items.filter(item =>
|
|
172
|
+
...schedule.items.filter(item =>
|
|
173
|
+
item.status === 'scheduled' || item.status === 'watch' ||
|
|
174
|
+
((item.status === 'retired' || item.status === 'retiring') && item.waiver?.active === true)),
|
|
155
175
|
...schedule.candidate_model_references.map(item => ({ ...item, status: 'unknown' })),
|
|
156
176
|
...schedule.unresolved_integrations.map(item => ({ ...item, status: 'unresolved' })),
|
|
157
177
|
]
|
|
@@ -174,7 +194,7 @@ export const formatCheck = ({ findings, bad, scannedFiles, days, scope }) => {
|
|
|
174
194
|
}
|
|
175
195
|
for (const f of findings) {
|
|
176
196
|
const where = `${f.file}:${f.line}`
|
|
177
|
-
out.push(humanFinding(f
|
|
197
|
+
out.push(humanFinding(f, `${marker(f.status)} ${where} ${f.matched} ${findingTail(f, days)}${f.via && f.via !== 'publisher' ? ` [via ${f.via}]` : ''}`))
|
|
178
198
|
}
|
|
179
199
|
if (bad.length) out.push(`\n${color('bold', `model-eol: ${bad.length} finding(s) at or past the ${days}-day threshold`)}`)
|
|
180
200
|
return out.join('\n')
|
|
@@ -187,7 +207,7 @@ export const formatInventory = (inventory, days) => {
|
|
|
187
207
|
if (inventory.model_references.length) {
|
|
188
208
|
out.push('\nModel references')
|
|
189
209
|
for (const item of inventory.model_references) {
|
|
190
|
-
out.push(humanFinding(item
|
|
210
|
+
out.push(humanFinding(item, `${marker(item.status)} ${item.file}:${item.line} ${item.matched} ${item.publisher}/${item.id} ${item.usage} ${findingTail(item, days)}`))
|
|
191
211
|
}
|
|
192
212
|
}
|
|
193
213
|
if (inventory.candidate_model_references.length) {
|
|
@@ -212,7 +232,7 @@ export const formatSchedule = (schedule, days) => {
|
|
|
212
232
|
if (incompleteNotes.length) out.push(`WARNING: scan incomplete; ${incompleteNotes.length} coverage-loss note(s) recorded`)
|
|
213
233
|
for (const item of schedule.items) {
|
|
214
234
|
const safeUntil = item.safe_until ? ` [safe until ${item.safe_until}]` : ''
|
|
215
|
-
out.push(humanFinding(item
|
|
235
|
+
out.push(humanFinding(item, `${marker(item.status)} ${item.shutdown ? shutdownText(item) : 'TBD'} ${item.file}:${item.line} ${item.matched} ${findingTail(item, days)} [${item.usage}]${safeUntil}`))
|
|
216
236
|
}
|
|
217
237
|
if (schedule.unresolved_integrations.length) {
|
|
218
238
|
out.push('\nUnresolved cloud/gateway references')
|
|
@@ -279,6 +299,7 @@ export const formatInventoryCycloneDX = inventory => {
|
|
|
279
299
|
identity,
|
|
280
300
|
item,
|
|
281
301
|
status: item.status,
|
|
302
|
+
occurrenceWaivers: new Map(),
|
|
282
303
|
usages: new Set(),
|
|
283
304
|
occurrences: new Set(),
|
|
284
305
|
}
|
|
@@ -287,12 +308,54 @@ export const formatInventoryCycloneDX = inventory => {
|
|
|
287
308
|
if ((statusSeverity.get(item.status) ?? 9) < (statusSeverity.get(group.status) ?? 9)) {
|
|
288
309
|
group.status = item.status
|
|
289
310
|
}
|
|
311
|
+
const location = `${item.file}#${item.line}`
|
|
312
|
+
const occurrenceWaiver = group.occurrenceWaivers.get(location) ?? { active: true, activeWaivers: [], expiredWaivers: [] }
|
|
313
|
+
if (item.waiver?.active === true) occurrenceWaiver.activeWaivers.push(item.waiver)
|
|
314
|
+
else {
|
|
315
|
+
occurrenceWaiver.active = false
|
|
316
|
+
if (item.waiver?.active === false) occurrenceWaiver.expiredWaivers.push(item.waiver)
|
|
317
|
+
}
|
|
318
|
+
group.occurrenceWaivers.set(location, occurrenceWaiver)
|
|
290
319
|
group.usages.add(item.usage)
|
|
291
|
-
group.occurrences.add(
|
|
320
|
+
group.occurrences.add(location)
|
|
292
321
|
}
|
|
293
322
|
|
|
294
323
|
const components = [...groups.values()].map(group => {
|
|
295
324
|
const { item, identity } = group
|
|
325
|
+
const occurrenceWaivers = [...group.occurrenceWaivers.values()]
|
|
326
|
+
const activeWaivers = occurrenceWaivers.flatMap(occurrence => occurrence.activeWaivers)
|
|
327
|
+
const expiredWaivers = occurrenceWaivers.flatMap(occurrence => occurrence.expiredWaivers)
|
|
328
|
+
const everyOccurrenceWaived = occurrenceWaivers.every(occurrence => occurrence.active && occurrence.activeWaivers.length > 0)
|
|
329
|
+
let waiverProperties = []
|
|
330
|
+
if (activeWaivers.length > 0 && !everyOccurrenceWaived) {
|
|
331
|
+
waiverProperties = [{ name: 'model-eol:waiver_active', value: 'partial' }]
|
|
332
|
+
} else if (everyOccurrenceWaived) {
|
|
333
|
+
const earliestExpiry = activeWaivers.map(waiver => waiver.expires).sort()[0]
|
|
334
|
+
const distinctWaivers = new Set(activeWaivers.map(waiver => JSON.stringify([waiver.owner, waiver.expires, waiver.reason])))
|
|
335
|
+
waiverProperties = [
|
|
336
|
+
{ name: 'model-eol:waiver_active', value: 'true' },
|
|
337
|
+
...(distinctWaivers.size === 1
|
|
338
|
+
? [
|
|
339
|
+
{ name: 'model-eol:waiver_owner', value: activeWaivers[0].owner },
|
|
340
|
+
{ name: 'model-eol:waiver_expires', value: earliestExpiry },
|
|
341
|
+
{ name: 'model-eol:waiver_reason', value: activeWaivers[0].reason },
|
|
342
|
+
]
|
|
343
|
+
: [{ name: 'model-eol:waiver_expires', value: earliestExpiry }]),
|
|
344
|
+
]
|
|
345
|
+
} else if (expiredWaivers.length > 0) {
|
|
346
|
+
const latestExpiry = expiredWaivers.map(waiver => waiver.expires).sort().at(-1)
|
|
347
|
+
const distinctWaivers = new Set(expiredWaivers.map(waiver => JSON.stringify([waiver.owner, waiver.expires, waiver.reason])))
|
|
348
|
+
waiverProperties = [
|
|
349
|
+
{ name: 'model-eol:waiver_active', value: 'false' },
|
|
350
|
+
...(distinctWaivers.size === 1
|
|
351
|
+
? [
|
|
352
|
+
{ name: 'model-eol:waiver_owner', value: expiredWaivers[0].owner },
|
|
353
|
+
{ name: 'model-eol:waiver_expires', value: latestExpiry },
|
|
354
|
+
{ name: 'model-eol:waiver_reason', value: expiredWaivers[0].reason },
|
|
355
|
+
]
|
|
356
|
+
: [{ name: 'model-eol:waiver_expires', value: latestExpiry }]),
|
|
357
|
+
]
|
|
358
|
+
}
|
|
296
359
|
return {
|
|
297
360
|
type: 'machine-learning-model',
|
|
298
361
|
'bom-ref': identity.bomRef,
|
|
@@ -319,6 +382,7 @@ export const formatInventoryCycloneDX = inventory => {
|
|
|
319
382
|
...(item.replacement_note
|
|
320
383
|
? [{ name: 'model-eol:replacement_note', value: item.replacement_note }]
|
|
321
384
|
: []),
|
|
385
|
+
...waiverProperties,
|
|
322
386
|
{ name: 'model-eol:usage', value: [...group.usages].sort().join(',') },
|
|
323
387
|
],
|
|
324
388
|
evidence: {
|
package/lib/scanner.mjs
CHANGED
|
@@ -638,6 +638,7 @@ export const scanTargets = ({
|
|
|
638
638
|
|
|
639
639
|
const pushModelRef = ({ file, line, lineText, matchIndex = null, matched, record, classification, route = null, mappedFrom = null }) => {
|
|
640
640
|
if (ignoresModel(file, matched, record.entry.id) || (mappedFrom && ignoresModel(file, mappedFrom, record.entry.id))) return
|
|
641
|
+
const waivers = policyForFile?.(path.resolve(file))?.waivers ?? []
|
|
641
642
|
const occurrence = [...lineText.matchAll(new RegExp(escapeRegExp(matched), 'g'))]
|
|
642
643
|
.findIndex(match => match.index === (matchIndex ?? lineText.indexOf(matched)))
|
|
643
644
|
modelRefs.push({
|
|
@@ -659,6 +660,7 @@ export const scanTargets = ({
|
|
|
659
660
|
occurrence: occurrence < 0 ? 0 : occurrence,
|
|
660
661
|
route_index: route?.index ?? null,
|
|
661
662
|
mapped_from: mappedFrom,
|
|
663
|
+
waivers,
|
|
662
664
|
})
|
|
663
665
|
}
|
|
664
666
|
|
|
@@ -64,7 +64,7 @@ export const normalizeDocumentType = value => {
|
|
|
64
64
|
const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
65
65
|
|
|
66
66
|
const configKeys = new Set([
|
|
67
|
-
'days', 'scope', 'via', 'feeds', 'ignore', 'overrides', 'routes', 'issues', 'eval',
|
|
67
|
+
'days', 'scope', 'via', 'feeds', 'ignore', 'waivers', 'overrides', 'routes', 'issues', 'eval',
|
|
68
68
|
])
|
|
69
69
|
|
|
70
70
|
export const detectDocumentType = (document, { file = null } = {}) => {
|
package/lib/validate-feed.mjs
CHANGED
|
@@ -9,8 +9,9 @@ export const MAX_TIMEOUT_MS = 3600000
|
|
|
9
9
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/
|
|
10
10
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
11
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'])
|
|
12
|
+
const DATE_PRECISIONS = new Set(['exact', 'earliest', 'tentative'])
|
|
13
13
|
const DISTRIBUTION_STATUSES = new Set(['active', 'legacy', 'extended-access', 'retired'])
|
|
14
|
+
const RESERVED_DISTRIBUTION_VIAS = new Set(['publisher', 'publisher-fallback'])
|
|
14
15
|
|
|
15
16
|
export const assertIsoDate = (value, label = 'date') => {
|
|
16
17
|
if (typeof value !== 'string' || !DATE_PATTERN.test(value)) {
|
|
@@ -181,7 +182,13 @@ export const validateFeed = feed => {
|
|
|
181
182
|
const shutdownValid = checkDate(model.shutdown, `${at}.shutdown`)
|
|
182
183
|
if (announcedValid && shutdownValid) validDates.push({ at, announced: model.announced, shutdown: model.shutdown })
|
|
183
184
|
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
|
|
185
|
+
if (typeof model.date_precision !== 'string' || !DATE_PRECISIONS.has(model.date_precision)) fail(`${at}.date_precision`, 'must be exact, earliest, or tentative')
|
|
186
|
+
}
|
|
187
|
+
if (model.date_precision === 'tentative' && model.announced !== undefined) {
|
|
188
|
+
fail(`${at}.announced`, 'must be absent when date_precision is tentative')
|
|
189
|
+
}
|
|
190
|
+
if (model.date_precision === 'tentative' && model.shutdown === undefined) {
|
|
191
|
+
fail(`${at}.shutdown`, 'is required when date_precision is tentative')
|
|
185
192
|
}
|
|
186
193
|
if (model.replacement !== undefined) checkIdentifier(model.replacement, `${at}.replacement`)
|
|
187
194
|
if (model.replacement_options !== undefined) {
|
|
@@ -210,6 +217,9 @@ export const validateFeed = feed => {
|
|
|
210
217
|
if (!checkObjectKeys(distribution, dAt, new Set(['via', 'announced', 'shutdown', 'date_precision', 'status', 'source']))) continue
|
|
211
218
|
if (!checkString(distribution.via, `${dAt}.via`, MAX_IDENTIFIER_LENGTH) || !distribution.via) fail(`${dAt}.via`, 'must be a non-empty string')
|
|
212
219
|
if (typeof distribution.via === 'string' && distribution.via.length > 0) {
|
|
220
|
+
if (RESERVED_DISTRIBUTION_VIAS.has(distribution.via.toLowerCase())) {
|
|
221
|
+
fail(`${dAt}.via`, 'must not use a reserved publisher clock name')
|
|
222
|
+
}
|
|
213
223
|
const previous = seenDistributionVias.get(distribution.via)
|
|
214
224
|
if (previous) fail(`${dAt}.via`, `duplicate distributor via "${distribution.via}" also used at ${previous}`)
|
|
215
225
|
else seenDistributionVias.set(distribution.via, `${dAt}.via`)
|
|
@@ -217,8 +227,17 @@ export const validateFeed = feed => {
|
|
|
217
227
|
const distributionAnnounced = checkDate(distribution.announced, `${dAt}.announced`)
|
|
218
228
|
const distributionShutdown = checkDate(distribution.shutdown, `${dAt}.shutdown`)
|
|
219
229
|
if (distributionAnnounced && distributionShutdown && distribution.shutdown < distribution.announced) fail(dAt, 'shutdown precedes announced')
|
|
220
|
-
if (distribution.date_precision !== undefined && (typeof distribution.date_precision !== 'string' || !DATE_PRECISIONS.has(distribution.date_precision))) fail(`${dAt}.date_precision`, 'must be exact or
|
|
230
|
+
if (distribution.date_precision !== undefined && (typeof distribution.date_precision !== 'string' || !DATE_PRECISIONS.has(distribution.date_precision))) fail(`${dAt}.date_precision`, 'must be exact, earliest, or tentative')
|
|
221
231
|
if (distribution.status !== undefined && (typeof distribution.status !== 'string' || !DISTRIBUTION_STATUSES.has(distribution.status))) fail(`${dAt}.status`, 'has an unsupported status')
|
|
232
|
+
if (distribution.date_precision === 'tentative' && distribution.announced !== undefined) {
|
|
233
|
+
fail(`${dAt}.announced`, 'must be absent when date_precision is tentative')
|
|
234
|
+
}
|
|
235
|
+
if (distribution.date_precision === 'tentative' && distribution.shutdown === undefined) {
|
|
236
|
+
fail(`${dAt}.shutdown`, 'is required when date_precision is tentative')
|
|
237
|
+
}
|
|
238
|
+
if (distribution.date_precision === 'tentative' && distribution.status === 'retired') {
|
|
239
|
+
fail(`${dAt}.status`, 'must not be retired when date_precision is tentative')
|
|
240
|
+
}
|
|
222
241
|
checkUrl(distribution.source, `${dAt}.source`)
|
|
223
242
|
if ((distribution.announced || distribution.shutdown) && !(distribution.source || model.source || feed.source)) fail(dAt, 'dated distribution needs a source')
|
|
224
243
|
}
|
package/package.json
CHANGED
|
@@ -54,6 +54,13 @@
|
|
|
54
54
|
},
|
|
55
55
|
"default": { "models": [], "paths": [] }
|
|
56
56
|
},
|
|
57
|
+
"waivers": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"maxItems": 500,
|
|
60
|
+
"items": { "$ref": "#/definitions/waiver" },
|
|
61
|
+
"default": [],
|
|
62
|
+
"description": "Owned, expiring exceptions that remain visible in reports while active."
|
|
63
|
+
},
|
|
57
64
|
"overrides": {
|
|
58
65
|
"type": "array",
|
|
59
66
|
"items": { "$ref": "#/definitions/pathOverride" },
|
|
@@ -120,6 +127,7 @@
|
|
|
120
127
|
"via": null,
|
|
121
128
|
"feeds": { "allow_vendored_fallback": false },
|
|
122
129
|
"ignore": { "models": [], "paths": [] },
|
|
130
|
+
"waivers": [],
|
|
123
131
|
"overrides": [],
|
|
124
132
|
"routes": [],
|
|
125
133
|
"issues": { "enabled": true },
|
|
@@ -144,6 +152,24 @@
|
|
|
144
152
|
"paths": { "$ref": "#/definitions/stringArray" }
|
|
145
153
|
}
|
|
146
154
|
},
|
|
155
|
+
"waiver": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"required": ["model", "reason", "owner", "expires"],
|
|
159
|
+
"properties": {
|
|
160
|
+
"model": { "type": "string", "minLength": 1 },
|
|
161
|
+
"paths": { "$ref": "#/definitions/stringArray" },
|
|
162
|
+
"via": { "type": "string", "minLength": 1 },
|
|
163
|
+
"reason": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
164
|
+
"owner": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
165
|
+
"expires": { "type": "string", "format": "date" }
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"waivers": {
|
|
169
|
+
"type": "array",
|
|
170
|
+
"maxItems": 500,
|
|
171
|
+
"items": { "$ref": "#/definitions/waiver" }
|
|
172
|
+
},
|
|
147
173
|
"pathOverride": {
|
|
148
174
|
"type": "object",
|
|
149
175
|
"additionalProperties": false,
|
|
@@ -153,7 +179,8 @@
|
|
|
153
179
|
"days": { "type": "integer", "minimum": 0 },
|
|
154
180
|
"scope": { "enum": ["direct", "all"] },
|
|
155
181
|
"via": { "type": ["string", "null"], "minLength": 1 },
|
|
156
|
-
"ignore": { "$ref": "#/definitions/ignore" }
|
|
182
|
+
"ignore": { "$ref": "#/definitions/ignore" },
|
|
183
|
+
"waivers": { "$ref": "#/definitions/waivers" }
|
|
157
184
|
}
|
|
158
185
|
},
|
|
159
186
|
"referenceRoute": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"confidence": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/confidence" },
|
|
33
33
|
"status": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/status" },
|
|
34
34
|
"shutdown": { "type": ["string", "null"], "format": "date" },
|
|
35
|
-
"date_precision": { "enum": ["exact", "earliest", null] },
|
|
35
|
+
"date_precision": { "enum": ["exact", "earliest", "tentative", null] },
|
|
36
36
|
"distribution_status": { "enum": ["active", "legacy", "extended-access", "retired", null] },
|
|
37
37
|
"via": { "type": ["string", "null"], "minLength": 1 },
|
|
38
38
|
"days": { "type": ["integer", "null"] },
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"threshold_days": { "type": "integer", "minimum": 0 },
|
|
48
48
|
"effective_scope": { "enum": ["all", "direct"] },
|
|
49
49
|
"requested_via": { "type": ["string", "null"], "minLength": 1 },
|
|
50
|
+
"waiver": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/waiver" },
|
|
50
51
|
"policy_provenance": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/policyProvenance" },
|
|
51
52
|
"mapped_from": { "type": "string", "minLength": 1 }
|
|
52
53
|
},
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"evidence": { "type": "string", "minLength": 1 },
|
|
61
61
|
"status": { "$ref": "#/definitions/status" },
|
|
62
62
|
"shutdown": { "type": ["string", "null"], "format": "date" },
|
|
63
|
-
"date_precision": { "enum": ["exact", "earliest", null] },
|
|
63
|
+
"date_precision": { "enum": ["exact", "earliest", "tentative", null] },
|
|
64
64
|
"distribution_status": { "enum": ["active", "legacy", "extended-access", "retired", null] },
|
|
65
65
|
"via": { "type": ["string", "null"], "minLength": 1 },
|
|
66
66
|
"requested_via": { "type": ["string", "null"], "minLength": 1 },
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" }
|
|
78
78
|
},
|
|
79
79
|
"replacement_note": { "type": "string", "minLength": 1 },
|
|
80
|
+
"waiver": { "$ref": "#/definitions/waiver" },
|
|
80
81
|
"source": { "type": ["string", "null"], "format": "uri" },
|
|
81
82
|
"policy": {
|
|
82
83
|
"type": ["object", "null"],
|
|
@@ -147,6 +148,17 @@
|
|
|
147
148
|
"route_index": { "type": ["integer", "null"], "minimum": 0 }
|
|
148
149
|
},
|
|
149
150
|
"additionalProperties": false
|
|
151
|
+
},
|
|
152
|
+
"waiver": {
|
|
153
|
+
"type": ["object", "null"],
|
|
154
|
+
"required": ["reason", "owner", "expires", "active"],
|
|
155
|
+
"properties": {
|
|
156
|
+
"reason": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
157
|
+
"owner": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
158
|
+
"expires": { "type": "string", "format": "date" },
|
|
159
|
+
"active": { "type": "boolean" }
|
|
160
|
+
},
|
|
161
|
+
"additionalProperties": false
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
164
|
}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"effective_scope": { "enum": ["all", "direct"] },
|
|
37
37
|
"policy_provenance": { "$ref": "#/definitions/policyProvenance" },
|
|
38
38
|
"mapped_from": { "type": "string", "minLength": 1 },
|
|
39
|
+
"waiver": { "$ref": "#/definitions/waiver" },
|
|
39
40
|
"sources": { "type": "array", "items": { "type": "string" } },
|
|
40
41
|
"notes": { "type": ["string", "null"] }
|
|
41
42
|
},
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
},
|
|
70
71
|
"replacement_note": { "type": "string" },
|
|
71
72
|
"provider": { "type": "string" },
|
|
73
|
+
"waiver": { "$ref": "#/definitions/waiver" },
|
|
72
74
|
"reason": { "type": "string", "minLength": 1 },
|
|
73
75
|
"sources": { "type": "array", "items": { "type": "string" } },
|
|
74
76
|
"notes": { "type": ["string", "null"] }
|
|
@@ -99,6 +101,17 @@
|
|
|
99
101
|
"route_index": { "type": ["integer", "null"], "minimum": 0 }
|
|
100
102
|
},
|
|
101
103
|
"additionalProperties": false
|
|
104
|
+
},
|
|
105
|
+
"waiver": {
|
|
106
|
+
"type": ["object", "null"],
|
|
107
|
+
"required": ["reason", "owner", "expires", "active"],
|
|
108
|
+
"properties": {
|
|
109
|
+
"reason": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
110
|
+
"owner": { "type": "string", "minLength": 1, "maxLength": 200 },
|
|
111
|
+
"expires": { "type": "string", "format": "date" },
|
|
112
|
+
"active": { "type": "boolean" }
|
|
113
|
+
},
|
|
114
|
+
"additionalProperties": false
|
|
102
115
|
}
|
|
103
116
|
}
|
|
104
117
|
}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"announced": { "$ref": "#/definitions/isoDate" },
|
|
42
42
|
"shutdown": { "$ref": "#/definitions/isoDate" },
|
|
43
|
-
"date_precision": { "enum": ["exact", "earliest"] },
|
|
43
|
+
"date_precision": { "enum": ["exact", "earliest", "tentative"] },
|
|
44
44
|
"replacement": { "type": "string", "maxLength": 256, "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
45
45
|
"replacement_options": {
|
|
46
46
|
"type": "array",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"via": { "type": "string", "minLength": 1 },
|
|
60
60
|
"announced": { "$ref": "#/definitions/isoDate" },
|
|
61
61
|
"shutdown": { "$ref": "#/definitions/isoDate" },
|
|
62
|
-
"date_precision": { "enum": ["exact", "earliest"] },
|
|
62
|
+
"date_precision": { "enum": ["exact", "earliest", "tentative"] },
|
|
63
63
|
"status": { "enum": ["active", "legacy", "extended-access", "retired"] },
|
|
64
64
|
"source": { "type": "string", "format": "uri" }
|
|
65
65
|
},
|