model-eol 0.9.1 → 0.10.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 +8 -4
- package/check.mjs +153 -62
- package/examples/workflows/model-eol.yml +3 -2
- package/lib/changed.mjs +627 -0
- package/lib/config.mjs +12 -7
- package/lib/glob.mjs +11 -3
- package/lib/reports.mjs +24 -1
- package/lib/scanner.mjs +98 -20
- package/package.json +2 -2
- package/schema/model-eol.check.schema.json +53 -1
package/lib/glob.mjs
CHANGED
|
@@ -29,7 +29,15 @@ export const globRegex = pattern => {
|
|
|
29
29
|
return new RegExp(`^${source}$`)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
const compiledGlobs = new Map()
|
|
33
|
+
const compiledGlob = pattern => {
|
|
34
|
+
if (!compiledGlobs.has(pattern)) compiledGlobs.set(pattern, globRegex(pattern))
|
|
35
|
+
return compiledGlobs.get(pattern)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const matchesGlob = (file, pattern) => compiledGlob(pattern).test(normalizeRepoPath(file))
|
|
33
39
|
|
|
34
|
-
export const matchesAnyGlob = (file, patterns = []) =>
|
|
35
|
-
|
|
40
|
+
export const matchesAnyGlob = (file, patterns = []) => {
|
|
41
|
+
const normalized = normalizeRepoPath(file)
|
|
42
|
+
return patterns.some(pattern => compiledGlob(pattern).test(normalized))
|
|
43
|
+
}
|
package/lib/reports.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isBad } from './feeds.mjs'
|
|
2
2
|
import { color } from './color.mjs'
|
|
3
3
|
import { incompleteScanNotes } from './scanner.mjs'
|
|
4
|
+
import { printable } from './verify.mjs'
|
|
4
5
|
|
|
5
6
|
export const marker = status =>
|
|
6
7
|
status === 'retired' ? '✗' :
|
|
@@ -196,6 +197,28 @@ export const buildAlert = schedule => {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
const CHANGED_INPUT_LABELS = new Map([
|
|
201
|
+
['config', 'config changed'],
|
|
202
|
+
['feeds', 'feeds changed'],
|
|
203
|
+
['rename', 'file renamed'],
|
|
204
|
+
['file', 'nearby lines changed'],
|
|
205
|
+
['untracked', 'untracked file'],
|
|
206
|
+
])
|
|
207
|
+
|
|
208
|
+
const changedTail = finding => {
|
|
209
|
+
const changed = finding.changed
|
|
210
|
+
if (changed?.reason === 'base-unavailable') return ` [base unavailable: ${printable(changed.message)}]`
|
|
211
|
+
if (changed?.reason !== 'newly-actionable') return ''
|
|
212
|
+
const inputs = changed.inputs.map(input => CHANGED_INPUT_LABELS.get(input) ?? printable(input)).join(', ')
|
|
213
|
+
const base = changed.base
|
|
214
|
+
const outOfScope = base && (base.status === 'retired' || base.status === 'retiring') &&
|
|
215
|
+
base.effective_scope === 'direct' && base.usage !== 'direct-api' && base.usage !== 'model-reference'
|
|
216
|
+
const was = !base
|
|
217
|
+
? 'no finding at base'
|
|
218
|
+
: `was ${base.status}${base.via && base.via !== 'publisher' ? ` via ${printable(base.via)}` : ''}${outOfScope ? ` but out of direct scope (${base.usage})` : ''} at base`
|
|
219
|
+
return ` [newly actionable: ${inputs}; ${was}]`
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
export const formatCheck = ({ findings, bad, scannedFiles, days, scope }) => {
|
|
200
223
|
const out = []
|
|
201
224
|
if (findings.length === 0) {
|
|
@@ -203,7 +226,7 @@ export const formatCheck = ({ findings, bad, scannedFiles, days, scope }) => {
|
|
|
203
226
|
}
|
|
204
227
|
for (const f of findings) {
|
|
205
228
|
const where = `${f.file}:${f.line}`
|
|
206
|
-
out.push(humanFinding(f, `${marker(f.status)} ${where} ${f.matched} ${findingTail(f, days)}${f.via && f.via !== 'publisher' ? ` [via ${f.via}]` : ''}`))
|
|
229
|
+
out.push(humanFinding(f, `${marker(f.status)} ${where} ${f.matched} ${findingTail(f, days)}${f.via && f.via !== 'publisher' ? ` [via ${f.via}]` : ''}${changedTail(f)}`))
|
|
207
230
|
}
|
|
208
231
|
if (bad.length) out.push(`\n${color('bold', `model-eol: ${bad.length} finding(s) at or past the ${days}-day threshold`)}`)
|
|
209
232
|
return out.join('\n')
|
package/lib/scanner.mjs
CHANGED
|
@@ -20,6 +20,7 @@ export const SKIP_DIRS = new Set([
|
|
|
20
20
|
'.tox', '.terraform', 'target', 'vendor', 'coverage', 'logs', 'tmp',
|
|
21
21
|
])
|
|
22
22
|
export const MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
23
|
+
export const GIT_MAX_BUFFER = 1024 * 1024 * 1024
|
|
23
24
|
export const MAX_FILES = 100_000
|
|
24
25
|
export const INCOMPLETE_SCAN_REASONS = new Set([
|
|
25
26
|
'file-too-large',
|
|
@@ -153,7 +154,7 @@ const isScannable = (file, includeDocs) => {
|
|
|
153
154
|
|
|
154
155
|
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
155
156
|
|
|
156
|
-
const displayPath = absolute => {
|
|
157
|
+
export const displayPath = absolute => {
|
|
157
158
|
const relative = path.relative(process.cwd(), absolute)
|
|
158
159
|
return relative && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)
|
|
159
160
|
? relative
|
|
@@ -227,14 +228,19 @@ const decodeGitQuotedPath = raw => {
|
|
|
227
228
|
return decoded
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
const
|
|
231
|
+
const decodeDiffPath = value => {
|
|
232
|
+
const line = value.endsWith('\r') ? value.slice(0, -1) : value
|
|
233
|
+
// Git appends a tab to ---/+++ names that contain a space; a literal tab in a name is always quoted.
|
|
234
|
+
const raw = line.endsWith('\t') ? line.slice(0, -1) : line
|
|
232
235
|
const decoded = raw.startsWith('"') ? decodeGitQuotedPath(raw) : raw
|
|
233
|
-
|
|
234
|
-
return decoded.startsWith('b/') ? decoded.slice(2) : decoded
|
|
236
|
+
return decoded === '/dev/null' ? null : decoded
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
|
|
239
|
+
const stripDiffPrefix = (decoded, prefix) => decoded !== null && decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded
|
|
240
|
+
|
|
241
|
+
export const parseDiffPath = value => stripDiffPrefix(decodeDiffPath(value), 'b/')
|
|
242
|
+
|
|
243
|
+
export const resolveChangedRepository = (targets, baseRef) => {
|
|
238
244
|
if (!baseRef) throw new Error('--changed requires a base ref')
|
|
239
245
|
const roots = new Set()
|
|
240
246
|
const resolvedTargets = []
|
|
@@ -279,42 +285,93 @@ export const addedLinesForTargets = (targets, baseRef) => {
|
|
|
279
285
|
const detail = (resolvedBase.stderr ?? '').trim()
|
|
280
286
|
throw new Error(`--changed could not resolve base ref "${baseRef}" to a commit${detail ? `: ${detail}` : ''}`)
|
|
281
287
|
}
|
|
288
|
+
return { gitRoot, baseCommit, relativeTargets }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const changedLinesForTargets = (targets, baseRef) => {
|
|
292
|
+
const { gitRoot, baseCommit, relativeTargets } = resolveChangedRepository(targets, baseRef)
|
|
293
|
+
const { GIT_DIFF_OPTS, ...env } = process.env
|
|
282
294
|
const result = spawnSync('git', [
|
|
283
|
-
'-c', 'core.quotePath=true',
|
|
284
|
-
'diff', '--no-color', '--src-prefix=a/', '--dst-prefix=b/',
|
|
295
|
+
'-c', 'core.quotePath=true', '-c', 'diff.relative=false', '-c', 'core.fsmonitor=false', '--literal-pathspecs',
|
|
296
|
+
'diff', '--no-color', '--src-prefix=a/', '--dst-prefix=b/', '--find-renames',
|
|
297
|
+
'--diff-algorithm=myers', '--indent-heuristic', '--inter-hunk-context=0',
|
|
285
298
|
'--text', '--no-ext-diff', '--no-textconv', '--unified=0', baseCommit, '--', ...relativeTargets,
|
|
286
299
|
], {
|
|
287
300
|
cwd: gitRoot,
|
|
288
301
|
encoding: 'utf8',
|
|
302
|
+
env,
|
|
289
303
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
304
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
290
305
|
})
|
|
291
306
|
if (result.error || result.status !== 0) {
|
|
292
|
-
const detail = (result.stderr ?? '').trim()
|
|
307
|
+
const detail = (result.stderr ?? '').trim() || result.error?.message || ''
|
|
293
308
|
throw new Error(`--changed git diff failed for base ref "${baseRef}"${detail ? `: ${detail}` : ''}`)
|
|
294
309
|
}
|
|
295
310
|
|
|
296
311
|
const added = new Set()
|
|
312
|
+
const files = new Map()
|
|
313
|
+
let header = null
|
|
297
314
|
let file = null
|
|
315
|
+
let record = null
|
|
298
316
|
let inHunk = false
|
|
299
317
|
let newLine = 0
|
|
300
318
|
let newLinesRemaining = 0
|
|
319
|
+
const recordFor = headPath => {
|
|
320
|
+
const absolute = path.resolve(gitRoot, headPath)
|
|
321
|
+
if (!files.has(absolute)) {
|
|
322
|
+
files.set(absolute, {
|
|
323
|
+
headPath,
|
|
324
|
+
basePath: header.renameFrom ?? header.minusPath,
|
|
325
|
+
renamed: header.renameFrom !== null && header.renameFrom !== headPath,
|
|
326
|
+
hunks: [],
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
return files.get(absolute)
|
|
330
|
+
}
|
|
331
|
+
const finishHeader = () => {
|
|
332
|
+
if (header && !record && header.renameTo !== null) recordFor(header.renameTo)
|
|
333
|
+
}
|
|
301
334
|
for (const line of result.stdout.split('\n')) {
|
|
302
335
|
if (line.startsWith('diff --git ')) {
|
|
336
|
+
finishHeader()
|
|
337
|
+
header = { minusPath: null, renameFrom: null, renameTo: null }
|
|
303
338
|
file = null
|
|
339
|
+
record = null
|
|
304
340
|
inHunk = false
|
|
305
341
|
newLinesRemaining = 0
|
|
306
342
|
continue
|
|
307
343
|
}
|
|
344
|
+
if (header && !inHunk) {
|
|
345
|
+
if (line.startsWith('rename from ')) {
|
|
346
|
+
header.renameFrom = decodeDiffPath(line.slice('rename from '.length))
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (line.startsWith('rename to ')) {
|
|
350
|
+
header.renameTo = decodeDiffPath(line.slice('rename to '.length))
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if (line.startsWith('--- ')) {
|
|
354
|
+
header.minusPath = stripDiffPrefix(decodeDiffPath(line.slice(4)), 'a/')
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
}
|
|
308
358
|
if (!inHunk && line.startsWith('+++ ')) {
|
|
309
359
|
const relative = parseDiffPath(line.slice(4))
|
|
310
360
|
file = relative ? path.resolve(gitRoot, relative) : null
|
|
361
|
+
record = relative && header ? recordFor(relative) : null
|
|
311
362
|
continue
|
|
312
363
|
}
|
|
313
|
-
const hunk = line.match(/^@@
|
|
364
|
+
const hunk = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/)
|
|
314
365
|
if (hunk) {
|
|
315
366
|
inHunk = true
|
|
316
|
-
newLine = Number(hunk[
|
|
317
|
-
newLinesRemaining = hunk[
|
|
367
|
+
newLine = Number(hunk[3])
|
|
368
|
+
newLinesRemaining = hunk[4] === undefined ? 1 : Number(hunk[4])
|
|
369
|
+
record?.hunks.push({
|
|
370
|
+
oldStart: Number(hunk[1]),
|
|
371
|
+
oldCount: hunk[2] === undefined ? 1 : Number(hunk[2]),
|
|
372
|
+
newStart: newLine,
|
|
373
|
+
newCount: newLinesRemaining,
|
|
374
|
+
})
|
|
318
375
|
continue
|
|
319
376
|
}
|
|
320
377
|
// Unified-diff metadata such as "\" does not
|
|
@@ -327,11 +384,26 @@ export const addedLinesForTargets = (targets, baseRef) => {
|
|
|
327
384
|
newLinesRemaining--
|
|
328
385
|
}
|
|
329
386
|
}
|
|
330
|
-
|
|
387
|
+
finishHeader()
|
|
388
|
+
return { gitRoot, baseCommit, relativeTargets, added, files }
|
|
331
389
|
}
|
|
332
390
|
|
|
391
|
+
export const addedLinesForTargets = (targets, baseRef) => changedLinesForTargets(targets, baseRef).added
|
|
392
|
+
|
|
393
|
+
export const baseLineFor = (hunks, line) => {
|
|
394
|
+
let offset = 0
|
|
395
|
+
for (const hunk of hunks) {
|
|
396
|
+
const newEnd = hunk.newCount === 0 ? hunk.newStart : hunk.newStart + hunk.newCount - 1
|
|
397
|
+
if (hunk.newCount > 0 && line >= hunk.newStart && line <= newEnd) return null
|
|
398
|
+
if (newEnd < line) offset += hunk.newCount - hunk.oldCount
|
|
399
|
+
}
|
|
400
|
+
return line - offset
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export const isAddedLine = (addedLines, file, line) => addedLines.has(changedKey(file, line))
|
|
404
|
+
|
|
333
405
|
export const filterFindingsToChanged = (findings, addedLines) =>
|
|
334
|
-
findings.filter(finding => addedLines
|
|
406
|
+
findings.filter(finding => isAddedLine(addedLines, finding.file, finding.line))
|
|
335
407
|
|
|
336
408
|
const collectFilesDetailed = (targets, {
|
|
337
409
|
includeDocs = false,
|
|
@@ -339,6 +411,7 @@ const collectFilesDetailed = (targets, {
|
|
|
339
411
|
ignoreRoots = [],
|
|
340
412
|
ignoredFiles = [],
|
|
341
413
|
policyForFile = null,
|
|
414
|
+
noGit = false,
|
|
342
415
|
} = {}) => {
|
|
343
416
|
const files = []
|
|
344
417
|
const notes = []
|
|
@@ -472,11 +545,12 @@ const collectFilesDetailed = (targets, {
|
|
|
472
545
|
const listGitFiles = (absoluteTarget, gitRoot) => {
|
|
473
546
|
const relativeTarget = path.relative(gitRoot, absoluteTarget) || '.'
|
|
474
547
|
const result = spawnSync('git', [
|
|
475
|
-
'ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', relativeTarget,
|
|
548
|
+
'--literal-pathspecs', 'ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', relativeTarget,
|
|
476
549
|
], {
|
|
477
550
|
cwd: gitRoot,
|
|
478
551
|
encoding: 'utf8',
|
|
479
552
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
553
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
480
554
|
})
|
|
481
555
|
if (result.error || result.status !== 0) {
|
|
482
556
|
const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`
|
|
@@ -488,11 +562,12 @@ const collectFilesDetailed = (targets, {
|
|
|
488
562
|
return
|
|
489
563
|
}
|
|
490
564
|
const tracked = spawnSync('git', [
|
|
491
|
-
'ls-files', '-z', '--cached', '--stage', '--', relativeTarget,
|
|
565
|
+
'--literal-pathspecs', 'ls-files', '-z', '--cached', '--stage', '--', relativeTarget,
|
|
492
566
|
], {
|
|
493
567
|
cwd: gitRoot,
|
|
494
568
|
encoding: 'utf8',
|
|
495
569
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
570
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
496
571
|
})
|
|
497
572
|
if (tracked.error || tracked.status !== 0) {
|
|
498
573
|
const detail = tracked.error?.message || tracked.stderr?.trim() || `exit ${tracked.status}`
|
|
@@ -557,7 +632,7 @@ const collectFilesDetailed = (targets, {
|
|
|
557
632
|
} catch {
|
|
558
633
|
resolvedTarget = absoluteTarget
|
|
559
634
|
}
|
|
560
|
-
const gitRoot = gitRootFor(resolvedTarget)
|
|
635
|
+
const gitRoot = noGit ? null : gitRootFor(resolvedTarget)
|
|
561
636
|
if (gitRoot) listGitFiles(resolvedTarget, gitRoot)
|
|
562
637
|
else walk(absoluteTarget)
|
|
563
638
|
}
|
|
@@ -579,8 +654,10 @@ export const scanTargets = ({
|
|
|
579
654
|
policyForFile = null,
|
|
580
655
|
routeForReference = null,
|
|
581
656
|
mappedRoutesForFile = null,
|
|
657
|
+
noGit = false,
|
|
658
|
+
classificationPathFor = null,
|
|
582
659
|
}) => {
|
|
583
|
-
const collected = collectFilesDetailed(targets, { includeDocs, ignorePaths, ignoreRoots, ignoredFiles, policyForFile })
|
|
660
|
+
const collected = collectFilesDetailed(targets, { includeDocs, ignorePaths, ignoreRoots, ignoredFiles, policyForFile, noGit })
|
|
584
661
|
if (collected.validTargets.length === 0) throw new Error('no valid targets remain')
|
|
585
662
|
const { files } = collected
|
|
586
663
|
const pattern = buildModelPattern(keys)
|
|
@@ -665,6 +742,7 @@ export const scanTargets = ({
|
|
|
665
742
|
}
|
|
666
743
|
|
|
667
744
|
for (const file of files) {
|
|
745
|
+
const classifiedFile = classificationPathFor ? classificationPathFor(file) : file
|
|
668
746
|
let bytes
|
|
669
747
|
try {
|
|
670
748
|
bytes = fs.readFileSync(file)
|
|
@@ -732,7 +810,7 @@ export const scanTargets = ({
|
|
|
732
810
|
const record = entries.get(m[1])
|
|
733
811
|
const route = routeForReference?.({ file, matched: m[1], id: record.entry.id }) ?? null
|
|
734
812
|
const classification = route ? routeClassification(route) : classifyRef({
|
|
735
|
-
file,
|
|
813
|
+
file: classifiedFile,
|
|
736
814
|
line: i + 1,
|
|
737
815
|
lines,
|
|
738
816
|
publisher: record.publisher,
|
|
@@ -755,7 +833,7 @@ export const scanTargets = ({
|
|
|
755
833
|
if (mappedSources.has(candidate) || isKnownModel(candidate, entries) || obviousCandidateNoise(candidate)) continue
|
|
756
834
|
if (ignoresModel(file, candidate)) continue
|
|
757
835
|
const classification = classifyRef({
|
|
758
|
-
file,
|
|
836
|
+
file: classifiedFile,
|
|
759
837
|
line: i + 1,
|
|
760
838
|
lines,
|
|
761
839
|
publisher: 'unknown',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "model-eol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "AI model deprecation feeds with a zero-dependency checker, inventory CLI, and migration bot.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"model-eol-bot": "bot/bot.mjs"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/run.mjs && node refresh/test/run.mjs && node bot/test/run.mjs && node scripts/test-action-contract.mjs && node scripts/test-document-validation.mjs && node scripts/test-receipts.mjs && node scripts/test-verify.mjs && node scripts/test-eval-harness.mjs && node bot/test/package.mjs && node scripts/test-feed-changelog.mjs && node scripts/test-public-site.mjs"
|
|
11
|
+
"test": "node test/run.mjs && node refresh/test/run.mjs && node bot/test/run.mjs && node scripts/test-action-contract.mjs && node scripts/test-document-validation.mjs && node scripts/test-receipts.mjs && node scripts/test-verify.mjs && node scripts/test-eval-harness.mjs && node bot/test/package.mjs && node scripts/test-feed-changelog.mjs && node scripts/test-public-site.mjs && node scripts/test-e2e-changed.mjs"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
@@ -49,9 +49,61 @@
|
|
|
49
49
|
"requested_via": { "type": ["string", "null"], "minLength": 1 },
|
|
50
50
|
"waiver": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/waiver" },
|
|
51
51
|
"policy_provenance": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/policyProvenance" },
|
|
52
|
-
"mapped_from": { "type": "string", "minLength": 1 }
|
|
52
|
+
"mapped_from": { "type": "string", "minLength": 1 },
|
|
53
|
+
"changed": { "$ref": "#/definitions/changed" }
|
|
53
54
|
},
|
|
54
55
|
"additionalProperties": false
|
|
56
|
+
},
|
|
57
|
+
"changed": {
|
|
58
|
+
"description": "Present only with --changed: why the finding is reported for this change.",
|
|
59
|
+
"oneOf": [
|
|
60
|
+
{
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["reason"],
|
|
63
|
+
"properties": { "reason": { "const": "added-line" } },
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"type": "object",
|
|
68
|
+
"required": ["reason", "inputs", "base"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"reason": { "const": "newly-actionable" },
|
|
71
|
+
"inputs": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"minItems": 1,
|
|
74
|
+
"uniqueItems": true,
|
|
75
|
+
"items": { "enum": ["config", "feeds", "rename", "file", "untracked"] }
|
|
76
|
+
},
|
|
77
|
+
"base": {
|
|
78
|
+
"oneOf": [
|
|
79
|
+
{ "type": "null" },
|
|
80
|
+
{
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["status", "via", "usage", "threshold_days", "effective_scope"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"status": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/status" },
|
|
85
|
+
"via": { "type": ["string", "null"], "minLength": 1 },
|
|
86
|
+
"usage": { "$ref": "https://thossullivan.github.io/model-eol/schema/0.1/model-eol.inventory.schema.json#/definitions/usage" },
|
|
87
|
+
"threshold_days": { "type": "integer", "minimum": 0 },
|
|
88
|
+
"effective_scope": { "enum": ["all", "direct"] }
|
|
89
|
+
},
|
|
90
|
+
"additionalProperties": false
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "object",
|
|
99
|
+
"required": ["reason", "message"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"reason": { "const": "base-unavailable" },
|
|
102
|
+
"message": { "type": "string", "minLength": 1 }
|
|
103
|
+
},
|
|
104
|
+
"additionalProperties": false
|
|
105
|
+
}
|
|
106
|
+
]
|
|
55
107
|
}
|
|
56
108
|
}
|
|
57
109
|
}
|