model-eol 0.3.0 → 0.4.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 +61 -20
- package/bot/bot.mjs +464 -71
- package/bot/lib/common.mjs +2 -0
- package/bot/lib/eval.mjs +6 -0
- package/bot/lib/git.mjs +11 -8
- package/bot/lib/github.mjs +10 -19
- package/bot.yml.example +95 -59
- package/check.mjs +81 -8
- package/feeds/amazon.json +1 -1
- package/feeds/anthropic.json +1 -1
- package/feeds/google.json +88 -5
- package/feeds/openai.json +11 -26
- package/lib/config.mjs +130 -5
- package/lib/feeds.mjs +38 -4
- package/lib/plan.mjs +27 -8
- package/lib/reports.mjs +24 -7
- package/lib/scanner.mjs +191 -73
- package/package.json +1 -1
- package/schema/model-eol.bot-config.schema.json +61 -0
- package/schema/model-eol.inventory.schema.json +21 -0
- package/schema/model-eol.plan.schema.json +22 -0
package/lib/scanner.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export const CODE_EXT = new Set([
|
|
|
9
9
|
'.c', '.cc', '.cjs', '.cpp', '.cs', '.cxx', '.go', '.h', '.hpp', '.java', '.js',
|
|
10
10
|
'.jsx', '.kt', '.kts', '.m', '.mjs', '.mm', '.php', '.py', '.rb', '.rs',
|
|
11
11
|
'.scala', '.sh', '.swift', '.ts', '.tsx',
|
|
12
|
-
'.cfg', '.env', '.ini', '.json', '.toml', '.yaml', '.yml',
|
|
12
|
+
'.baml', '.cfg', '.env', '.ini', '.json', '.toml', '.yaml', '.yml',
|
|
13
13
|
'.sql', '.tf', '.tfvars',
|
|
14
14
|
])
|
|
15
15
|
export const DOC_EXT = new Set(['.md', '.mdx', '.txt', '.rst'])
|
|
@@ -34,25 +34,25 @@ export const incompleteScanNotes = notes => notes.filter(note => INCOMPLETE_SCAN
|
|
|
34
34
|
export const scanIsIncomplete = notes => incompleteScanNotes(notes).length > 0
|
|
35
35
|
|
|
36
36
|
const directSignals = [
|
|
37
|
-
['openai', /\b(OPENAI_API_KEY|api\.openai\.com|from\s+openai\s+import|import\s+OpenAI\s+from\s+['"]openai['"]|require\(['"]openai['"]\)|new\s+OpenAI\s*\()/i, 'OpenAI direct SDK/API'],
|
|
38
|
-
['anthropic', /\b(ANTHROPIC_API_KEY|api\.anthropic\.com|@anthropic-ai\/sdk|from\s+anthropic\s+import|import\s+Anthropic\s+from\s+['"]@anthropic-ai\/sdk['"]|new\s+Anthropic\s*\()/i, 'Anthropic direct SDK/API'],
|
|
39
|
-
['google-gemini', /\b(GEMINI_API_KEY|GOOGLE_API_KEY|generativelanguage\.googleapis\.com|@google\/genai|google-generativeai|GoogleGenerativeAI)\b/i, 'Gemini direct SDK/API'],
|
|
40
|
-
['mistral', /\b(MISTRAL_API_KEY|api\.mistral\.ai|@mistralai\/mistralai|from\s+mistralai\s+import|MistralClient)\b/i, 'Mistral direct SDK/API'],
|
|
41
|
-
['cohere', /\b(COHERE_API_KEY|api\.cohere\.ai|from\s+cohere\s+import|new\s+CohereClient\s*\()/i, 'Cohere direct SDK/API'],
|
|
42
|
-
['xai', /\b(XAI_API_KEY|api\.x\.ai)\b/i, 'xAI direct SDK/API'],
|
|
37
|
+
['openai', /\b(OPENAI_API_KEY|api\.openai\.com|from\s+openai\s+import|import\s+OpenAI\s+from\s+['"]openai['"]|require\(['"]openai['"]\)|new\s+OpenAI\s*\(|provider\s+['"]?openai\b)/i, 'OpenAI direct SDK/API or BAML provider'],
|
|
38
|
+
['anthropic', /\b(ANTHROPIC_API_KEY|api\.anthropic\.com|@anthropic-ai\/sdk|from\s+anthropic\s+import|import\s+Anthropic\s+from\s+['"]@anthropic-ai\/sdk['"]|new\s+Anthropic\s*\(|provider\s+['"]?anthropic\b)/i, 'Anthropic direct SDK/API or BAML provider'],
|
|
39
|
+
['google-gemini', /\b(GEMINI_API_KEY|GOOGLE_API_KEY|generativelanguage\.googleapis\.com|@google\/genai|google-generativeai|GoogleGenerativeAI|provider\s+['"]?google-ai\b)\b/i, 'Gemini direct SDK/API or BAML provider'],
|
|
40
|
+
['mistral', /\b(MISTRAL_API_KEY|api\.mistral\.ai|@mistralai\/mistralai|from\s+mistralai\s+import|MistralClient|provider\s+['"]?mistral\b)\b/i, 'Mistral direct SDK/API or BAML provider'],
|
|
41
|
+
['cohere', /\b(COHERE_API_KEY|api\.cohere\.ai|from\s+cohere\s+import|new\s+CohereClient\s*\(|provider\s+['"]?cohere\b)/i, 'Cohere direct SDK/API or BAML provider'],
|
|
42
|
+
['xai', /\b(XAI_API_KEY|api\.x\.ai|provider\s+['"]?xai\b)\b/i, 'xAI direct SDK/API or BAML provider'],
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
const cloudSignals = [
|
|
46
|
-
['azure-ai-foundry', /\b(AZURE_OPENAI|AzureOpenAI|@azure\/openai|azure\.ai\.openai|azure-ai-foundry|AZURE_AI)\b/i, 'Azure Foundry/OpenAI deployment reference'],
|
|
47
|
-
['aws-bedrock', /\b(BEDROCK_MODEL_ID|bedrock-runtime|BedrockRuntime|InvokeModel|invoke_model|amazon\.bedrock)\b/i, 'Amazon Bedrock reference'],
|
|
48
|
-
['vertex-ai', /\b(VERTEX_AI|GOOGLE_CLOUD_PROJECT|aiplatform|vertexai|VertexAI|PredictionServiceClient|publishers\/google\/models)\b/i, 'Vertex AI reference'],
|
|
46
|
+
['azure-ai-foundry', /\b(AZURE_OPENAI|AzureOpenAI|@azure\/openai|azure\.ai\.openai|azure-ai-foundry|AZURE_AI|provider\s+['"]?azure-openai\b)\b/i, 'Azure Foundry/OpenAI deployment reference'],
|
|
47
|
+
['aws-bedrock', /\b(BEDROCK_MODEL_ID|bedrock-runtime|BedrockRuntime|InvokeModel|invoke_model|amazon\.bedrock|provider\s+['"]?aws-bedrock\b)\b/i, 'Amazon Bedrock reference'],
|
|
48
|
+
['vertex-ai', /\b(VERTEX_AI|GOOGLE_CLOUD_PROJECT|aiplatform|vertexai|VertexAI|PredictionServiceClient|publishers\/google\/models|provider\s+['"]?vertex-ai\b)\b/i, 'Vertex AI reference'],
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
const gatewaySignals = [
|
|
52
|
-
['openrouter', /\b(OPENROUTER_API_KEY|openrouter\.ai|openrouter
|
|
53
|
-
['litellm', /\b(LITELLM|litellm|LiteLLM)\b/, 'LiteLLM gateway reference'],
|
|
54
|
-
['portkey', /\b(PORTKEY_API_KEY|portkey|Portkey)\b/, 'Portkey gateway reference'],
|
|
55
|
-
['ai-gateway', /\b(AI_GATEWAY|Vercel AI Gateway|ai-gateway)\b/i, 'AI gateway reference'],
|
|
52
|
+
['openrouter', /\b(OPENROUTER_API_KEY|openrouter\.ai|openrouter\/|provider\s+['"]?openrouter\b)\b/i, 'OpenRouter gateway reference'],
|
|
53
|
+
['litellm', /\b(LITELLM|litellm|LiteLLM|provider\s+['"]?litellm\b)\b/, 'LiteLLM gateway reference'],
|
|
54
|
+
['portkey', /\b(PORTKEY_API_KEY|portkey|Portkey|provider\s+['"]?portkey\b)\b/, 'Portkey gateway reference'],
|
|
55
|
+
['ai-gateway', /\b(AI_GATEWAY|Vercel AI Gateway|ai-gateway|provider\s+['"]?ai-gateway\b)\b/i, 'AI gateway reference'],
|
|
56
56
|
]
|
|
57
57
|
|
|
58
58
|
const allSignals = [
|
|
@@ -72,6 +72,13 @@ const candidatePatterns = [
|
|
|
72
72
|
/["']((?:gpt|o[1345]|claude|gemini|mistral|command|grok|llama|deepseek|qwen|nova|embed)[A-Za-z0-9._:/-]{4,120})["']/gi,
|
|
73
73
|
]
|
|
74
74
|
|
|
75
|
+
const obviousCandidateNoise = value =>
|
|
76
|
+
/^[A-Z][A-Z0-9_]+$/.test(value) ||
|
|
77
|
+
/^commands?_[a-z0-9_]+$/i.test(value) ||
|
|
78
|
+
/^claude_code[._]/i.test(value) ||
|
|
79
|
+
/\.(?:md|mdx|json|ya?ml|toml|txt|rst)$/i.test(value) ||
|
|
80
|
+
/\.(?:token|cost)\.usage$/i.test(value)
|
|
81
|
+
|
|
75
82
|
const suffixes = value => {
|
|
76
83
|
const parts = value.split(/[\/.:]/).filter(Boolean)
|
|
77
84
|
return parts.flatMap((_, i) => [parts.slice(i).join('/'), parts.slice(i).join('.'), parts.slice(i).join(':')])
|
|
@@ -88,7 +95,7 @@ const findCandidates = (lineText, entries) => {
|
|
|
88
95
|
let m
|
|
89
96
|
while ((m = pattern.exec(lineText)) !== null) {
|
|
90
97
|
const value = m[1].trim()
|
|
91
|
-
if (seen.has(value)
|
|
98
|
+
if (seen.has(value)) continue
|
|
92
99
|
seen.add(value)
|
|
93
100
|
candidates.push(value)
|
|
94
101
|
}
|
|
@@ -96,6 +103,10 @@ const findCandidates = (lineText, entries) => {
|
|
|
96
103
|
return candidates
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
const publisherForProvider = provider => provider === 'google-gemini' ? 'google' : provider
|
|
107
|
+
|
|
108
|
+
const providerMatchesPublisher = (provider, publisher) => publisherForProvider(provider) === publisher
|
|
109
|
+
|
|
99
110
|
const classifyRef = ({ file, line, lines, publisher }) => {
|
|
100
111
|
const start = Math.max(0, line - 8)
|
|
101
112
|
const end = Math.min(lines.length, line + 4)
|
|
@@ -109,8 +120,12 @@ const classifyRef = ({ file, line, lines, publisher }) => {
|
|
|
109
120
|
const nearest = signals[0]
|
|
110
121
|
if (nearest) {
|
|
111
122
|
const hasCompetingRoute = signals.some(signal => signal.usage === 'cloud-provider' || signal.usage === 'gateway')
|
|
112
|
-
const
|
|
113
|
-
|
|
123
|
+
const providerMatches = nearest.usage !== 'direct-api' || providerMatchesPublisher(nearest.provider, publisher)
|
|
124
|
+
const confidence = nearest.usage === 'direct-api' && !hasCompetingRoute && providerMatches ? 'high' : 'medium'
|
|
125
|
+
const evidence = providerMatches
|
|
126
|
+
? nearest.evidence
|
|
127
|
+
: `${nearest.evidence}; signal provider ${nearest.provider} differs from model publisher ${publisher}`
|
|
128
|
+
return { usage: nearest.usage, provider: nearest.provider, confidence, evidence }
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
const lowerFile = file.toLowerCase()
|
|
@@ -248,6 +263,7 @@ const collectFilesDetailed = (targets, {
|
|
|
248
263
|
ignorePaths = [],
|
|
249
264
|
ignoreRoots = [],
|
|
250
265
|
ignoredFiles = [],
|
|
266
|
+
policyForFile = null,
|
|
251
267
|
} = {}) => {
|
|
252
268
|
const files = []
|
|
253
269
|
const notes = []
|
|
@@ -284,8 +300,15 @@ const collectFilesDetailed = (targets, {
|
|
|
284
300
|
})
|
|
285
301
|
const isIgnoredFile = absolute => {
|
|
286
302
|
const resolved = canonicalPath(absolute)
|
|
287
|
-
|
|
303
|
+
if (excludedFiles.has(resolved) || repoPathsFor(resolved).some(matchesIgnoredRepoPath)) return true
|
|
304
|
+
const policy = policyForFile?.(resolved)
|
|
305
|
+
return Boolean(policy?.repoPath && matchesIgnoredRepoPathWithPatterns(policy.repoPath, policy.ignore?.paths ?? []))
|
|
288
306
|
}
|
|
307
|
+
const matchesIgnoredRepoPathWithPatterns = (repoPath, patterns) => matchesAnyGlob(repoPath, patterns) ||
|
|
308
|
+
patterns.some(pattern => {
|
|
309
|
+
const normalizedPattern = normalizeRepoPath(pattern).replace(/\/+$/, '')
|
|
310
|
+
return normalizedPattern && !/[*?]/.test(normalizedPattern) && repoPath.startsWith(`${normalizedPattern}/`)
|
|
311
|
+
})
|
|
289
312
|
const isIgnoredDirectoryTree = absolute => repoPathsFor(absolute).some(repoPath =>
|
|
290
313
|
ignorePaths.some(pattern => {
|
|
291
314
|
const normalizedPattern = normalizeRepoPath(pattern)
|
|
@@ -426,29 +449,92 @@ export const scanTargets = ({
|
|
|
426
449
|
ignorePaths = [],
|
|
427
450
|
ignoreRoots = [],
|
|
428
451
|
ignoredFiles = [],
|
|
452
|
+
policyForFile = null,
|
|
453
|
+
routeForReference = null,
|
|
454
|
+
mappedRoutesForFile = null,
|
|
429
455
|
}) => {
|
|
430
|
-
const collected = collectFilesDetailed(targets, { includeDocs, ignorePaths, ignoreRoots, ignoredFiles })
|
|
456
|
+
const collected = collectFilesDetailed(targets, { includeDocs, ignorePaths, ignoreRoots, ignoredFiles, policyForFile })
|
|
431
457
|
if (collected.validTargets.length === 0) throw new Error('no valid targets remain')
|
|
432
458
|
const { files } = collected
|
|
433
459
|
const pattern = buildModelPattern(keys)
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
const ignoredSets = models => ({
|
|
461
|
+
keys: new Set(models),
|
|
462
|
+
canonicalIds: new Set(models.map(key => entries.get(key)?.entry.id ?? key)),
|
|
463
|
+
})
|
|
464
|
+
const baseIgnored = ignoredSets(ignoreModels)
|
|
465
|
+
const ignoresModel = (file, matched, id = null) => {
|
|
466
|
+
const configured = policyForFile?.(path.resolve(file))?.ignore?.models ?? []
|
|
467
|
+
const dynamicIgnored = configured.length ? ignoredSets(configured) : null
|
|
468
|
+
return baseIgnored.keys.has(matched) || (id !== null && baseIgnored.canonicalIds.has(id)) ||
|
|
469
|
+
dynamicIgnored?.keys.has(matched) || (id !== null && dynamicIgnored?.canonicalIds.has(id))
|
|
470
|
+
}
|
|
438
471
|
const modelRefs = []
|
|
439
472
|
const candidateRefs = []
|
|
440
473
|
const integrationHints = []
|
|
441
474
|
|
|
442
|
-
const
|
|
475
|
+
const modelEolDocument = (file, text) => {
|
|
443
476
|
if (!file.endsWith('.json')) return false
|
|
444
477
|
try {
|
|
445
|
-
const
|
|
446
|
-
|
|
478
|
+
const value = JSON.parse(text)
|
|
479
|
+
const productSchema = [value?.spec, value?.schema, value?.plan_schema]
|
|
480
|
+
.some(identifier => typeof identifier === 'string' && (identifier.startsWith('model-eol/') || identifier.startsWith('model-eol.')))
|
|
481
|
+
const generatedCycloneDx = value?.bomFormat === 'CycloneDX' &&
|
|
482
|
+
value?.metadata?.properties?.some(property =>
|
|
483
|
+
property?.name === 'model-eol:generator' && property?.value === 'model-eol/inventory-cyclonedx@0.1')
|
|
484
|
+
return productSchema || generatedCycloneDx
|
|
447
485
|
} catch {
|
|
448
486
|
return false
|
|
449
487
|
}
|
|
450
488
|
}
|
|
451
489
|
|
|
490
|
+
const generatedSource = text => {
|
|
491
|
+
const header = text.slice(0, 8192)
|
|
492
|
+
return /This file was generated by BAML/i.test(header) ||
|
|
493
|
+
/Code generated .* DO NOT EDIT/i.test(header) ||
|
|
494
|
+
/^\s*(?:[/#*;-]+\s*)?@generated\b/im.test(header) ||
|
|
495
|
+
/AUTO[- ]GENERATED FILE[\s\S]{0,240}DO NOT EDIT/i.test(header)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const routeClassification = route => {
|
|
499
|
+
const usage = cloudSignals.some(([provider]) => provider === route.via)
|
|
500
|
+
? 'cloud-provider'
|
|
501
|
+
: gatewaySignals.some(([provider]) => provider === route.via)
|
|
502
|
+
? 'gateway'
|
|
503
|
+
: 'model-reference'
|
|
504
|
+
return {
|
|
505
|
+
usage,
|
|
506
|
+
provider: route.via,
|
|
507
|
+
confidence: 'high',
|
|
508
|
+
evidence: `configured route ${route.index} via ${route.via}`,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const pushModelRef = ({ file, line, lineText, matchIndex = null, matched, record, classification, route = null, mappedFrom = null }) => {
|
|
513
|
+
if (ignoresModel(file, matched, record.entry.id) || (mappedFrom && ignoresModel(file, mappedFrom, record.entry.id))) return
|
|
514
|
+
const occurrence = [...lineText.matchAll(new RegExp(escapeRegExp(matched), 'g'))]
|
|
515
|
+
.findIndex(match => match.index === (matchIndex ?? lineText.indexOf(matched)))
|
|
516
|
+
modelRefs.push({
|
|
517
|
+
kind: 'model-reference',
|
|
518
|
+
file,
|
|
519
|
+
line,
|
|
520
|
+
matched,
|
|
521
|
+
id: record.entry.id,
|
|
522
|
+
entry: record.entry,
|
|
523
|
+
publisher: record.publisher,
|
|
524
|
+
source: record.source,
|
|
525
|
+
feedNote: record.feedNote,
|
|
526
|
+
policy: record.policy,
|
|
527
|
+
generated: record.generated,
|
|
528
|
+
usage: classification.usage,
|
|
529
|
+
resolved_provider: classification.provider,
|
|
530
|
+
confidence: classification.confidence,
|
|
531
|
+
evidence: classification.evidence,
|
|
532
|
+
occurrence: occurrence < 0 ? 0 : occurrence,
|
|
533
|
+
route_index: route?.index ?? null,
|
|
534
|
+
mapped_from: mappedFrom,
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
452
538
|
for (const file of files) {
|
|
453
539
|
let text
|
|
454
540
|
try {
|
|
@@ -457,61 +543,81 @@ export const scanTargets = ({
|
|
|
457
543
|
collected.notes.push({ reason: 'unreadable-file', file, message: e.message })
|
|
458
544
|
continue
|
|
459
545
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
546
|
+
// Product artifacts and generated clients describe model IDs but are not
|
|
547
|
+
// authoritative usage or safe migration targets.
|
|
548
|
+
if (modelEolDocument(file, text)) {
|
|
549
|
+
collected.notes.push({ reason: 'model-eol-document-skipped', file })
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
if (generatedSource(text)) {
|
|
553
|
+
collected.notes.push({ reason: 'generated-artifact-skipped', file })
|
|
463
554
|
continue
|
|
464
555
|
}
|
|
465
556
|
const lines = text.split('\n')
|
|
466
557
|
for (const [i, lineText] of lines.entries()) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
})
|
|
558
|
+
const signals = findSignals(lineText)
|
|
559
|
+
const candidates = findCandidates(lineText, entries)
|
|
560
|
+
const mappedSources = new Set()
|
|
561
|
+
const mappedSpans = []
|
|
562
|
+
const resolvedProviders = new Set()
|
|
563
|
+
const selectedMappings = new Map()
|
|
564
|
+
for (const configuredRoute of mappedRoutesForFile?.(file) ?? []) {
|
|
565
|
+
const route = routeForReference?.({ file, matched: configuredRoute.match, id: null }) ?? null
|
|
566
|
+
if (!route?.model) continue
|
|
567
|
+
selectedMappings.set(`${route.index}:${route.match}`, route)
|
|
568
|
+
}
|
|
569
|
+
for (const route of selectedMappings.values()) {
|
|
570
|
+
const record = entries.get(route.model)
|
|
571
|
+
if (!record) continue
|
|
572
|
+
const exactRoutePattern = new RegExp(`(?<![A-Za-z0-9._:/-])${escapeRegExp(route.match)}(?![A-Za-z0-9._:/-])`, 'g')
|
|
573
|
+
for (const match of lineText.matchAll(exactRoutePattern)) {
|
|
574
|
+
mappedSources.add(route.match)
|
|
575
|
+
mappedSpans.push({ start: match.index, end: match.index + route.match.length })
|
|
576
|
+
resolvedProviders.add(route.via)
|
|
577
|
+
pushModelRef({
|
|
578
|
+
file,
|
|
579
|
+
line: i + 1,
|
|
580
|
+
lineText,
|
|
581
|
+
matchIndex: match.index,
|
|
582
|
+
matched: route.match,
|
|
583
|
+
record,
|
|
584
|
+
classification: routeClassification(route),
|
|
585
|
+
route,
|
|
586
|
+
mappedFrom: route.match,
|
|
587
|
+
})
|
|
588
|
+
}
|
|
477
589
|
}
|
|
478
590
|
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
generated: record.generated,
|
|
505
|
-
usage: classification.usage,
|
|
506
|
-
resolved_provider: classification.provider,
|
|
507
|
-
confidence: classification.confidence,
|
|
508
|
-
evidence: classification.evidence,
|
|
509
|
-
occurrence: occurrence < 0 ? 0 : occurrence,
|
|
510
|
-
})
|
|
591
|
+
if (pattern) {
|
|
592
|
+
pattern.lastIndex = 0
|
|
593
|
+
let m
|
|
594
|
+
while ((m = pattern.exec(lineText)) !== null) {
|
|
595
|
+
if (mappedSpans.some(span => m.index >= span.start && m.index + m[1].length <= span.end)) continue
|
|
596
|
+
const record = entries.get(m[1])
|
|
597
|
+
const route = routeForReference?.({ file, matched: m[1], id: record.entry.id }) ?? null
|
|
598
|
+
const classification = route ? routeClassification(route) : classifyRef({
|
|
599
|
+
file,
|
|
600
|
+
line: i + 1,
|
|
601
|
+
lines,
|
|
602
|
+
publisher: record.publisher,
|
|
603
|
+
})
|
|
604
|
+
if (route) resolvedProviders.add(route.via)
|
|
605
|
+
pushModelRef({
|
|
606
|
+
file,
|
|
607
|
+
line: i + 1,
|
|
608
|
+
lineText,
|
|
609
|
+
matchIndex: m.index,
|
|
610
|
+
matched: m[1],
|
|
611
|
+
record,
|
|
612
|
+
classification,
|
|
613
|
+
route,
|
|
614
|
+
})
|
|
615
|
+
}
|
|
511
616
|
}
|
|
512
617
|
|
|
513
|
-
for (const candidate of
|
|
514
|
-
if (
|
|
618
|
+
for (const candidate of candidates) {
|
|
619
|
+
if (mappedSources.has(candidate) || isKnownModel(candidate, entries) || obviousCandidateNoise(candidate)) continue
|
|
620
|
+
if (ignoresModel(file, candidate)) continue
|
|
515
621
|
const classification = classifyRef({
|
|
516
622
|
file,
|
|
517
623
|
line: i + 1,
|
|
@@ -530,6 +636,18 @@ export const scanTargets = ({
|
|
|
530
636
|
reason: 'model-like string not present in loaded feeds',
|
|
531
637
|
})
|
|
532
638
|
}
|
|
639
|
+
for (const signal of signals) {
|
|
640
|
+
if (resolvedProviders.has(signal.provider)) continue
|
|
641
|
+
integrationHints.push({
|
|
642
|
+
kind: 'integration-hint',
|
|
643
|
+
usage: signal.usage,
|
|
644
|
+
provider: signal.provider,
|
|
645
|
+
file,
|
|
646
|
+
line: i + 1,
|
|
647
|
+
matched: signal.matched,
|
|
648
|
+
evidence: signal.evidence,
|
|
649
|
+
})
|
|
650
|
+
}
|
|
533
651
|
}
|
|
534
652
|
}
|
|
535
653
|
|
package/package.json
CHANGED
|
@@ -54,6 +54,18 @@
|
|
|
54
54
|
},
|
|
55
55
|
"default": { "models": [], "paths": [] }
|
|
56
56
|
},
|
|
57
|
+
"overrides": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": { "$ref": "#/definitions/pathOverride" },
|
|
60
|
+
"default": [],
|
|
61
|
+
"description": "Path-scoped policy overrides. Matching entries apply in array order; later scalar values win and ignore lists are additive."
|
|
62
|
+
},
|
|
63
|
+
"routes": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"items": { "$ref": "#/definitions/referenceRoute" },
|
|
66
|
+
"default": [],
|
|
67
|
+
"description": "Path- and optionally reference-scoped distributor routes. The last matching route wins. Exact match/model pairs can resolve repository deployment aliases to canonical feed IDs."
|
|
68
|
+
},
|
|
57
69
|
"issues": {
|
|
58
70
|
"type": "object",
|
|
59
71
|
"additionalProperties": false,
|
|
@@ -104,7 +116,56 @@
|
|
|
104
116
|
"via": null,
|
|
105
117
|
"feeds": { "allow_vendored_fallback": false },
|
|
106
118
|
"ignore": { "models": [], "paths": [] },
|
|
119
|
+
"overrides": [],
|
|
120
|
+
"routes": [],
|
|
107
121
|
"issues": { "enabled": true },
|
|
108
122
|
"eval": { "command": null, "timeout_ms": 600000, "max_report_bytes": 65536, "pass_env": [] }
|
|
123
|
+
},
|
|
124
|
+
"definitions": {
|
|
125
|
+
"stringArray": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"items": { "type": "string", "minLength": 1 }
|
|
128
|
+
},
|
|
129
|
+
"nonEmptyStringArray": {
|
|
130
|
+
"allOf": [
|
|
131
|
+
{ "$ref": "#/definitions/stringArray" },
|
|
132
|
+
{ "minItems": 1 }
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"ignore": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"properties": {
|
|
139
|
+
"models": { "$ref": "#/definitions/stringArray" },
|
|
140
|
+
"paths": { "$ref": "#/definitions/stringArray" }
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"pathOverride": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"required": ["paths"],
|
|
147
|
+
"properties": {
|
|
148
|
+
"paths": { "$ref": "#/definitions/nonEmptyStringArray" },
|
|
149
|
+
"days": { "type": "integer", "minimum": 0 },
|
|
150
|
+
"scope": { "enum": ["direct", "all"] },
|
|
151
|
+
"via": { "type": ["string", "null"], "minLength": 1 },
|
|
152
|
+
"ignore": { "$ref": "#/definitions/ignore" }
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"referenceRoute": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"required": ["paths", "via"],
|
|
159
|
+
"properties": {
|
|
160
|
+
"paths": { "$ref": "#/definitions/nonEmptyStringArray" },
|
|
161
|
+
"via": { "type": "string", "minLength": 1 },
|
|
162
|
+
"match": { "type": "string", "minLength": 1 },
|
|
163
|
+
"model": { "type": "string", "minLength": 1 }
|
|
164
|
+
},
|
|
165
|
+
"dependencies": {
|
|
166
|
+
"match": ["model"],
|
|
167
|
+
"model": ["match"]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
109
170
|
}
|
|
110
171
|
}
|
|
@@ -60,8 +60,15 @@
|
|
|
60
60
|
"evidence": { "type": "string" },
|
|
61
61
|
"status": { "$ref": "#/definitions/status" },
|
|
62
62
|
"shutdown": { "type": ["string", "null"] },
|
|
63
|
+
"date_precision": { "enum": ["exact", "earliest", null] },
|
|
64
|
+
"distribution_status": { "enum": ["active", "legacy", "extended-access", "retired", null] },
|
|
63
65
|
"via": { "type": ["string", "null"] },
|
|
66
|
+
"requested_via": { "type": ["string", "null"] },
|
|
64
67
|
"days": { "type": ["integer", "null"] },
|
|
68
|
+
"threshold_days": { "type": "integer", "minimum": 0 },
|
|
69
|
+
"effective_scope": { "enum": ["all", "direct"] },
|
|
70
|
+
"policy_provenance": { "$ref": "#/definitions/policyProvenance" },
|
|
71
|
+
"mapped_from": { "type": "string", "minLength": 1 },
|
|
65
72
|
"safe_until": { "type": ["string", "null"], "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
66
73
|
"replacement": { "type": ["string", "null"] },
|
|
67
74
|
"source": { "type": ["string", "null"] },
|
|
@@ -92,6 +99,8 @@
|
|
|
92
99
|
"resolved_provider": { "type": "string" },
|
|
93
100
|
"confidence": { "$ref": "#/definitions/confidence" },
|
|
94
101
|
"evidence": { "type": "string" },
|
|
102
|
+
"effective_scope": { "enum": ["all", "direct"] },
|
|
103
|
+
"policy_provenance": { "$ref": "#/definitions/policyProvenance" },
|
|
95
104
|
"reason": { "type": "string" }
|
|
96
105
|
}
|
|
97
106
|
}
|
|
@@ -125,6 +134,18 @@
|
|
|
125
134
|
"limit_files": { "type": "integer", "minimum": 0 }
|
|
126
135
|
},
|
|
127
136
|
"additionalProperties": false
|
|
137
|
+
},
|
|
138
|
+
"policyProvenance": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"required": ["override_indexes", "route_index"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"override_indexes": {
|
|
143
|
+
"type": "array",
|
|
144
|
+
"items": { "type": "integer", "minimum": 0 }
|
|
145
|
+
},
|
|
146
|
+
"route_index": { "type": ["integer", "null"], "minimum": 0 }
|
|
147
|
+
},
|
|
148
|
+
"additionalProperties": false
|
|
128
149
|
}
|
|
129
150
|
}
|
|
130
151
|
}
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"shutdown": { "type": "string" },
|
|
32
32
|
"days": { "type": "integer" },
|
|
33
33
|
"status": { "enum": ["retired", "retiring"] },
|
|
34
|
+
"requested_via": { "type": ["string", "null"] },
|
|
35
|
+
"threshold_days": { "type": "integer", "minimum": 0 },
|
|
36
|
+
"effective_scope": { "enum": ["all", "direct"] },
|
|
37
|
+
"policy_provenance": { "$ref": "#/definitions/policyProvenance" },
|
|
38
|
+
"mapped_from": { "type": "string", "minLength": 1 },
|
|
34
39
|
"sources": { "type": "array", "items": { "type": "string" } },
|
|
35
40
|
"notes": { "type": ["string", "null"] }
|
|
36
41
|
},
|
|
@@ -50,7 +55,12 @@
|
|
|
50
55
|
"status": { "enum": ["retired", "retiring", "scheduled", "watch", "ok"] },
|
|
51
56
|
"shutdown": { "type": ["string", "null"] },
|
|
52
57
|
"via": { "type": ["string", "null"] },
|
|
58
|
+
"requested_via": { "type": ["string", "null"] },
|
|
53
59
|
"days": { "type": ["integer", "null"] },
|
|
60
|
+
"threshold_days": { "type": "integer", "minimum": 0 },
|
|
61
|
+
"effective_scope": { "enum": ["all", "direct"] },
|
|
62
|
+
"policy_provenance": { "$ref": "#/definitions/policyProvenance" },
|
|
63
|
+
"mapped_from": { "type": "string", "minLength": 1 },
|
|
54
64
|
"replacement": { "type": ["string", "null"] },
|
|
55
65
|
"replacement_options": {
|
|
56
66
|
"type": "array",
|
|
@@ -77,6 +87,18 @@
|
|
|
77
87
|
"limit_files": { "type": "integer", "minimum": 0 }
|
|
78
88
|
},
|
|
79
89
|
"additionalProperties": false
|
|
90
|
+
},
|
|
91
|
+
"policyProvenance": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"required": ["override_indexes", "route_index"],
|
|
94
|
+
"properties": {
|
|
95
|
+
"override_indexes": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"items": { "type": "integer", "minimum": 0 }
|
|
98
|
+
},
|
|
99
|
+
"route_index": { "type": ["integer", "null"], "minimum": 0 }
|
|
100
|
+
},
|
|
101
|
+
"additionalProperties": false
|
|
80
102
|
}
|
|
81
103
|
}
|
|
82
104
|
}
|