model-eol 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -19
- 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 +84 -51
- 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/bot/lib/common.mjs
CHANGED
|
@@ -67,6 +67,8 @@ export const parseMetadata = body => {
|
|
|
67
67
|
if (metadata.replacement !== undefined && metadata.replacement !== null && typeof metadata.replacement !== 'string') return null
|
|
68
68
|
if (metadata.replacement_options !== undefined && (!Array.isArray(metadata.replacement_options) || metadata.replacement_options.some(option => typeof option !== 'string'))) return null
|
|
69
69
|
if (metadata.replacement_note !== undefined && metadata.replacement_note !== null && typeof metadata.replacement_note !== 'string') return null
|
|
70
|
+
if (metadata.base_sha !== undefined && metadata.base_sha !== null && (typeof metadata.base_sha !== 'string' || !/^[0-9a-f]{40,64}$/.test(metadata.base_sha))) return null
|
|
71
|
+
if (metadata.stale_closed !== undefined && metadata.stale_closed !== true) return null
|
|
70
72
|
if (typeof metadata.feed_digest !== 'string' || !metadata.feed_digest) return null
|
|
71
73
|
if (!Object.hasOwn(metadata, 'head_sha') || (metadata.head_sha !== null && typeof metadata.head_sha !== 'string')) return null
|
|
72
74
|
if (metadata.channel !== undefined && (typeof metadata.channel !== 'string' || !metadata.channel)) return null
|
package/bot/lib/eval.mjs
CHANGED
|
@@ -9,6 +9,12 @@ const truncateReport = (bytes, limit, truncated) => {
|
|
|
9
9
|
return Buffer.concat([bytes.subarray(0, limit - marker.length), marker]).subarray(0, limit).toString('utf8')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export const capReport = (report, limit) => {
|
|
13
|
+
if (report === null || report === undefined) return null
|
|
14
|
+
const bytes = Buffer.from(String(report), 'utf8')
|
|
15
|
+
return truncateReport(bytes, Math.max(0, limit), bytes.length > limit)
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export const readReportCapped = (file, limit) => {
|
|
13
19
|
const cap = Math.max(0, limit)
|
|
14
20
|
let lstat
|
package/bot/lib/git.mjs
CHANGED
|
@@ -105,23 +105,26 @@ export const verifyBotBranch = (cwd, branch, expectedHead, auth = null) => {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export const pushBranch = (cwd, branch, expectedHead = null, auth = null) => {
|
|
108
|
+
export const pushBranch = (cwd, branch, expectedHead = null, auth = null, { allowMissing = false } = {}) => {
|
|
109
109
|
const destination = `HEAD:refs/heads/${branch}`
|
|
110
110
|
if (expectedHead !== null) {
|
|
111
111
|
const state = verifyBotBranch(cwd, branch, expectedHead, auth)
|
|
112
|
-
if (!state.safe) {
|
|
112
|
+
if (!state.safe && !(allowMissing && !state.error && state.head === null)) {
|
|
113
113
|
const error = new Error(state.error || `refusing force-push: branch head ${state.head || 'missing'} or committer email ${state.committerEmail || 'missing'} failed bot lease checks`)
|
|
114
114
|
error.code = 'MODEL_EOL_BRANCH_STAND_DOWN'
|
|
115
115
|
error.currentHead = state.head
|
|
116
116
|
error.committerEmail = state.committerEmail
|
|
117
117
|
throw error
|
|
118
118
|
}
|
|
119
|
-
run(cwd, [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
if (state.head === null) run(cwd, ['push', 'origin', destination], false, auth)
|
|
120
|
+
else {
|
|
121
|
+
run(cwd, [
|
|
122
|
+
'push',
|
|
123
|
+
`--force-with-lease=refs/heads/${branch}:${expectedHead}`,
|
|
124
|
+
'origin',
|
|
125
|
+
destination,
|
|
126
|
+
], false, auth)
|
|
127
|
+
}
|
|
125
128
|
} else {
|
|
126
129
|
run(cwd, ['push', 'origin', destination], false, auth)
|
|
127
130
|
}
|
package/bot/lib/github.mjs
CHANGED
|
@@ -81,7 +81,7 @@ export class GitHubClient {
|
|
|
81
81
|
} catch (error) {
|
|
82
82
|
if (error.status !== 404) {
|
|
83
83
|
this.modelEolLabelAvailable = false
|
|
84
|
-
this.warn(`model-eol: warning: could not check the model-eol label;
|
|
84
|
+
this.warn(`model-eol: warning: could not check the model-eol label; refusing to publish untrusted work (${error.message})`)
|
|
85
85
|
return false
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -97,11 +97,11 @@ export class GitHubClient {
|
|
|
97
97
|
return true
|
|
98
98
|
} catch {
|
|
99
99
|
// Fall through to the warning below. Another writer may have raced us,
|
|
100
|
-
// but
|
|
100
|
+
// but ownership must be confirmed before publishing work.
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
this.modelEolLabelAvailable = false
|
|
104
|
-
this.warn(`model-eol: warning: could not create the model-eol label;
|
|
104
|
+
this.warn(`model-eol: warning: could not create the model-eol label; refusing to publish untrusted work (${error.message})`)
|
|
105
105
|
return false
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -113,7 +113,7 @@ export class GitHubClient {
|
|
|
113
113
|
return true
|
|
114
114
|
} catch (error) {
|
|
115
115
|
this.modelEolLabelAvailable = false
|
|
116
|
-
this.warn(`model-eol: warning: could not label #${number};
|
|
116
|
+
this.warn(`model-eol: warning: could not label #${number}; refusing to leave it open as trusted bot work (${error.message})`)
|
|
117
117
|
return false
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -157,7 +157,10 @@ export class GitHubClient {
|
|
|
157
157
|
async createPull(payload) {
|
|
158
158
|
const { data } = await this.request('POST', `/repos/${this.repo}/pulls`, payload)
|
|
159
159
|
if (data?.number === undefined) throw new Error('GitHub pull request response did not contain a number')
|
|
160
|
-
await this.addModelEolLabel(data.number)
|
|
160
|
+
if (!await this.addModelEolLabel(data.number)) {
|
|
161
|
+
try { await this.updatePull(data.number, { state: 'closed' }) } catch {}
|
|
162
|
+
throw new Error(`refusing unlabeled bot pull request #${data.number}; it was closed because ownership could not be established`)
|
|
163
|
+
}
|
|
161
164
|
return data
|
|
162
165
|
}
|
|
163
166
|
|
|
@@ -170,20 +173,8 @@ export class GitHubClient {
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
async createIssue(payload) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
: { ...payload, labels: (payload.labels ?? []).filter(label => label !== MODEL_EOL_LABEL.name) }
|
|
176
|
-
try {
|
|
177
|
-
return (await this.request('POST', `/repos/${this.repo}/issues`, body)).data
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (error.status !== 422 || !(body.labels ?? []).includes(MODEL_EOL_LABEL.name)) throw error
|
|
180
|
-
this.modelEolLabelAvailable = false
|
|
181
|
-
this.warn(`model-eol: warning: issue labels were rejected; retrying without the model-eol label (${error.message})`)
|
|
182
|
-
return (await this.request('POST', `/repos/${this.repo}/issues`, {
|
|
183
|
-
...body,
|
|
184
|
-
labels: body.labels.filter(label => label !== MODEL_EOL_LABEL.name),
|
|
185
|
-
})).data
|
|
186
|
-
}
|
|
176
|
+
if (!this.modelEolLabelAvailable) throw new Error('refusing unlabeled bot issue; model-eol label is unavailable')
|
|
177
|
+
return (await this.request('POST', `/repos/${this.repo}/issues`, payload)).data
|
|
187
178
|
}
|
|
188
179
|
|
|
189
180
|
async updateIssue(number, payload) {
|
package/bot.yml.example
CHANGED
|
@@ -15,12 +15,11 @@ env:
|
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
17
|
plan:
|
|
18
|
-
name:
|
|
18
|
+
name: build trusted plan
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
permissions:
|
|
21
21
|
contents: read
|
|
22
22
|
steps:
|
|
23
|
-
# The eval job must never hold write tokens.
|
|
24
23
|
- uses: actions/checkout@v4
|
|
25
24
|
with:
|
|
26
25
|
persist-credentials: false
|
|
@@ -29,51 +28,75 @@ jobs:
|
|
|
29
28
|
node-version: 20
|
|
30
29
|
- name: Build the versioned plan
|
|
31
30
|
run: |
|
|
31
|
+
mkdir -p "$RUNNER_TEMP/model-eol-plan"
|
|
32
|
+
MODEL_EOL_VERSION="$(npm view "$MODEL_EOL_PACKAGE" version --json | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s);const r=Array.isArray(v)?v.at(-1):v;if(typeof r!=="string")process.exit(1);process.stdout.write(r)})')"
|
|
33
|
+
if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
34
|
+
echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
printf '%s\n' "$MODEL_EOL_VERSION" > "$RUNNER_TEMP/model-eol-plan/model-eol-version"
|
|
32
38
|
PLAN_ARGS=(plan .)
|
|
33
39
|
if [ ! -f .model-eol.json ]; then PLAN_ARGS+=(--days 90 --scope direct); fi
|
|
34
|
-
npx --yes --package="
|
|
35
|
-
- name:
|
|
40
|
+
npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol "${PLAN_ARGS[@]}" > "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json"
|
|
41
|
+
- name: Upload plan
|
|
42
|
+
uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: model-eol-plan
|
|
45
|
+
path: |
|
|
46
|
+
${{ runner.temp }}/model-eol-plan/model-eol-plan.json
|
|
47
|
+
${{ runner.temp }}/model-eol-plan/model-eol-version
|
|
48
|
+
if-no-files-found: error
|
|
49
|
+
|
|
50
|
+
evaluate:
|
|
51
|
+
name: evaluate isolated migrations
|
|
52
|
+
needs: plan
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
permissions:
|
|
55
|
+
contents: read
|
|
56
|
+
steps:
|
|
57
|
+
# Provider credentials exist only in this read-only job. The evaluator
|
|
58
|
+
# forwards only names explicitly allowed by eval.pass_env.
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
with:
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
- uses: actions/setup-node@v4
|
|
63
|
+
with:
|
|
64
|
+
node-version: 20
|
|
65
|
+
- name: Download trusted plan
|
|
66
|
+
uses: actions/download-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: model-eol-plan
|
|
69
|
+
path: ${{ runner.temp }}/model-eol-plan
|
|
70
|
+
- name: Apply and evaluate each migration independently
|
|
36
71
|
env:
|
|
37
72
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
38
73
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
74
|
+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
75
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
76
|
+
# A non-empty repository variable overrides eval.command. Otherwise
|
|
77
|
+
# the strictly validated command in .model-eol.json is used.
|
|
39
78
|
MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
|
|
40
|
-
MODEL_EOL_PLAN: ${{ github.workspace }}/model-eol-plan.json
|
|
41
|
-
MODEL_EOL_REPORT: ${{ github.workspace }}/model-eol-report.md
|
|
42
79
|
run: |
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const config = JSON.parse(fs.readFileSync(".model-eol.json", "utf8"))
|
|
49
|
-
if (typeof config.eval?.command === "string") process.stdout.write(config.eval.command)
|
|
50
|
-
')"
|
|
51
|
-
fi
|
|
52
|
-
# Supply a provider-backed harness for this repository. It may read the
|
|
53
|
-
# plan and write Markdown to MODEL_EOL_REPORT, but this job has read-only
|
|
54
|
-
# contents permission and the publish job receives no provider keys.
|
|
55
|
-
if [ -n "${MODEL_EOL_EVAL_COMMAND:-}" ]; then
|
|
56
|
-
set +e
|
|
57
|
-
sh -c "$MODEL_EOL_EVAL_COMMAND"
|
|
58
|
-
eval_status=$?
|
|
59
|
-
set -e
|
|
60
|
-
printf '%s\n' "$eval_status" > model-eol-eval-exit-code
|
|
61
|
-
else
|
|
62
|
-
: > "$MODEL_EOL_REPORT"
|
|
63
|
-
printf '0\n' > model-eol-eval-exit-code
|
|
80
|
+
mkdir -p "$RUNNER_TEMP/model-eol-eval"
|
|
81
|
+
MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
|
|
82
|
+
if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
83
|
+
echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
|
|
84
|
+
exit 1
|
|
64
85
|
fi
|
|
65
|
-
|
|
86
|
+
npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot evaluate \
|
|
87
|
+
--target-dir . \
|
|
88
|
+
--plan-file "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json" \
|
|
89
|
+
--output-file "$RUNNER_TEMP/model-eol-eval/model-eol-eval-results.json"
|
|
90
|
+
- name: Upload bounded eval results
|
|
66
91
|
uses: actions/upload-artifact@v4
|
|
67
92
|
with:
|
|
68
|
-
name: model-eol-
|
|
69
|
-
path:
|
|
70
|
-
|
|
71
|
-
model-eol-report.md
|
|
72
|
-
model-eol-eval-exit-code
|
|
93
|
+
name: model-eol-eval-results
|
|
94
|
+
path: ${{ runner.temp }}/model-eol-eval/model-eol-eval-results.json
|
|
95
|
+
if-no-files-found: error
|
|
73
96
|
|
|
74
97
|
publish:
|
|
75
|
-
name:
|
|
76
|
-
needs: plan
|
|
98
|
+
name: reconcile migration work
|
|
99
|
+
needs: [plan, evaluate]
|
|
77
100
|
runs-on: ubuntu-latest
|
|
78
101
|
permissions:
|
|
79
102
|
contents: write
|
|
@@ -88,25 +111,35 @@ jobs:
|
|
|
88
111
|
- uses: actions/setup-node@v4
|
|
89
112
|
with:
|
|
90
113
|
node-version: 20
|
|
91
|
-
- name: Download
|
|
114
|
+
- name: Download trusted plan
|
|
115
|
+
uses: actions/download-artifact@v4
|
|
116
|
+
with:
|
|
117
|
+
name: model-eol-plan
|
|
118
|
+
path: ${{ runner.temp }}/model-eol-plan
|
|
119
|
+
- name: Download eval results
|
|
92
120
|
uses: actions/download-artifact@v4
|
|
93
121
|
with:
|
|
94
|
-
name: model-eol-
|
|
95
|
-
|
|
122
|
+
name: model-eol-eval-results
|
|
123
|
+
path: ${{ runner.temp }}/model-eol-eval
|
|
124
|
+
- name: Publish and reconcile model-eol work
|
|
96
125
|
env:
|
|
97
126
|
GITHUB_TOKEN: ${{ secrets.MODEL_EOL_BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
|
98
|
-
MODEL_EOL_PLAN_FILE: ${{
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
MODEL_EOL_PLAN_FILE: ${{ runner.temp }}/model-eol-plan/model-eol-plan.json
|
|
128
|
+
MODEL_EOL_EVAL_RESULTS_FILE: ${{ runner.temp }}/model-eol-eval/model-eol-eval-results.json
|
|
129
|
+
# The write job receives the non-secret command text only so it can
|
|
130
|
+
# verify the eval configuration digest. It receives no provider keys.
|
|
131
|
+
MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
|
|
101
132
|
MODEL_EOL_TOKEN_KIND: ${{ secrets.MODEL_EOL_BOT_TOKEN && 'external-token' || 'github-token' }}
|
|
102
|
-
run:
|
|
133
|
+
run: |
|
|
134
|
+
MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
|
|
135
|
+
if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
136
|
+
echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot --repo "${{ github.repository }}" --target-dir .
|
|
103
140
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
# Keep provider API keys only in the read-only plan/eval job and never pass them to
|
|
110
|
-
# the publish job.
|
|
111
|
-
# Set feeds.allow_vendored_fallback: true in .model-eol.json only for degraded report-only runs; the default is false.
|
|
112
|
-
# Independent concurrent invocations need external mutual exclusion; this tool does not lock.
|
|
141
|
+
# A PR made with GITHUB_TOKEN does not trigger pull_request workflows. For
|
|
142
|
+
# required checks, store a narrowly scoped fine-grained PAT or GitHub App token
|
|
143
|
+
# as MODEL_EOL_BOT_TOKEN. Keep provider keys out of the publish job.
|
|
144
|
+
# Set feeds.allow_vendored_fallback: true only for degraded report-only runs.
|
|
145
|
+
# Independent concurrent invocations need external mutual exclusion.
|
package/check.mjs
CHANGED
|
@@ -19,8 +19,18 @@ import path from 'node:path'
|
|
|
19
19
|
|
|
20
20
|
import { applyPlan } from './lib/apply.mjs'
|
|
21
21
|
import { parseCliArgs } from './lib/cli.mjs'
|
|
22
|
-
import {
|
|
23
|
-
|
|
22
|
+
import {
|
|
23
|
+
CLI_DEFAULT_CONFIG,
|
|
24
|
+
DEFAULT_CONFIG,
|
|
25
|
+
configForRepoPath,
|
|
26
|
+
configuredVias,
|
|
27
|
+
loadConfig,
|
|
28
|
+
mappedRoutesForRepoPath,
|
|
29
|
+
normalizeConfig,
|
|
30
|
+
routeForRepoReference,
|
|
31
|
+
} from './lib/config.mjs'
|
|
32
|
+
import { BUILTIN_CHANNELS, findingFromRef, isBad, loadFeeds } from './lib/feeds.mjs'
|
|
33
|
+
import { normalizeRepoPath } from './lib/glob.mjs'
|
|
24
34
|
import { buildPlan } from './lib/plan.mjs'
|
|
25
35
|
import {
|
|
26
36
|
buildAlert,
|
|
@@ -73,6 +83,15 @@ const configForTargets = ({ targets, explicit }) => {
|
|
|
73
83
|
return { file: files[0] ?? null, roots }
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
const repoPathForFile = (file, roots) => {
|
|
87
|
+
const absolute = path.resolve(file)
|
|
88
|
+
const candidates = roots
|
|
89
|
+
.map(root => ({ root, relative: path.relative(root, absolute) }))
|
|
90
|
+
.filter(({ relative }) => relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
91
|
+
.sort((a, b) => b.root.length - a.root.length)
|
|
92
|
+
return normalizeRepoPath(candidates[0]?.relative ?? absolute)
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
const main = () => {
|
|
77
96
|
const COMMANDS = new Set(['check', 'inventory', 'schedule', 'alert', 'plan', 'apply', 'help'])
|
|
78
97
|
const args = process.argv.slice(2)
|
|
@@ -222,6 +241,37 @@ if (feedData.entries.size === 0) {
|
|
|
222
241
|
return 2
|
|
223
242
|
}
|
|
224
243
|
|
|
244
|
+
const allowedVias = new Set([...BUILTIN_CHANNELS, ...feedData.vias])
|
|
245
|
+
const unknownVia = [values.via, ...configuredVias(repositoryConfig)]
|
|
246
|
+
.find(via => via && !allowedVias.has(via))
|
|
247
|
+
if (unknownVia) {
|
|
248
|
+
console.error(`unknown lifecycle channel "${unknownVia}"; known values: ${[...allowedVias].sort().join(', ')}`)
|
|
249
|
+
return 2
|
|
250
|
+
}
|
|
251
|
+
const unresolvedRoute = repositoryConfig.routes.find(route => route.model && !feedData.entries.has(route.model))
|
|
252
|
+
if (unresolvedRoute) {
|
|
253
|
+
console.error(`configured route model "${unresolvedRoute.model}" is not present in loaded feeds`)
|
|
254
|
+
return 2
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const policyCache = new Map()
|
|
258
|
+
const policyForFile = file => {
|
|
259
|
+
const absolute = path.resolve(file)
|
|
260
|
+
if (!policyCache.has(absolute)) {
|
|
261
|
+
const repoPath = repoPathForFile(absolute, configLocation.roots)
|
|
262
|
+
policyCache.set(absolute, { ...configForRepoPath(repositoryConfig, repoPath), repoPath })
|
|
263
|
+
}
|
|
264
|
+
return policyCache.get(absolute)
|
|
265
|
+
}
|
|
266
|
+
const routeForReference = ({ file, matched, id }) => {
|
|
267
|
+
const policy = policyForFile(file)
|
|
268
|
+
return routeForRepoReference(repositoryConfig, policy.repoPath, { matched, id })
|
|
269
|
+
}
|
|
270
|
+
const mappedRoutesForFile = file => {
|
|
271
|
+
const policy = policyForFile(file)
|
|
272
|
+
return mappedRoutesForRepoPath(repositoryConfig, policy.repoPath)
|
|
273
|
+
}
|
|
274
|
+
|
|
225
275
|
let scan
|
|
226
276
|
try {
|
|
227
277
|
scan = scanTargets({
|
|
@@ -229,10 +279,11 @@ try {
|
|
|
229
279
|
entries: feedData.entries,
|
|
230
280
|
keys: feedData.keys,
|
|
231
281
|
includeDocs: INCLUDE_DOCS,
|
|
232
|
-
ignoreModels: repositoryConfig.ignore.models,
|
|
233
|
-
ignorePaths: repositoryConfig.ignore.paths,
|
|
234
282
|
ignoreRoots: configLocation.roots,
|
|
235
283
|
ignoredFiles: configLocation.file ? [configLocation.file] : [],
|
|
284
|
+
policyForFile,
|
|
285
|
+
routeForReference,
|
|
286
|
+
mappedRoutesForFile,
|
|
236
287
|
})
|
|
237
288
|
} catch (e) {
|
|
238
289
|
console.error(`scan failed: ${e.message}`)
|
|
@@ -250,10 +301,32 @@ if (incompleteNotes.length && (command === 'check' || command === 'plan')) {
|
|
|
250
301
|
if (!ALLOW_INCOMPLETE) return 2
|
|
251
302
|
}
|
|
252
303
|
|
|
253
|
-
const findings = scan.modelRefs.map(ref =>
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
:
|
|
304
|
+
const findings = scan.modelRefs.map(ref => {
|
|
305
|
+
const policy = policyForFile(ref.file)
|
|
306
|
+
const route = routeForReference({ file: ref.file, matched: ref.matched, id: ref.id })
|
|
307
|
+
const days = values.days === undefined ? policy.days : DAYS
|
|
308
|
+
const scope = values.scope === undefined ? policy.scope : SCOPE
|
|
309
|
+
const via = values.via === undefined ? (route?.via ?? policy.via) : VIA
|
|
310
|
+
const finding = findingFromRef(ref, { days, via })
|
|
311
|
+
finding.effective_scope = scope
|
|
312
|
+
finding.requested_via = via
|
|
313
|
+
if (policy.override_indexes.length || route) {
|
|
314
|
+
finding.policy_provenance = {
|
|
315
|
+
override_indexes: policy.override_indexes,
|
|
316
|
+
route_index: route?.index ?? null,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return finding
|
|
320
|
+
})
|
|
321
|
+
for (const candidate of scan.candidateRefs) {
|
|
322
|
+
const policy = policyForFile(candidate.file)
|
|
323
|
+
candidate.effective_scope = values.scope === undefined ? policy.scope : SCOPE
|
|
324
|
+
if (policy.override_indexes.length) {
|
|
325
|
+
candidate.policy_provenance = { override_indexes: policy.override_indexes, route_index: null }
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const checkFindings = findings.filter(finding =>
|
|
329
|
+
finding.effective_scope !== 'direct' || finding.usage === 'direct-api' || finding.usage === 'model-reference')
|
|
257
330
|
let changedFindings = checkFindings
|
|
258
331
|
if (CHANGED_BASE !== null) {
|
|
259
332
|
try {
|
package/feeds/amazon.json
CHANGED
package/feeds/anthropic.json
CHANGED
package/feeds/google.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"spec": "model-eol/0.1",
|
|
3
3
|
"publisher": "google",
|
|
4
|
-
"generated": "2026-08-
|
|
4
|
+
"generated": "2026-08-18T00:47:44.931Z",
|
|
5
5
|
"source": "https://ai.google.dev/gemini-api/docs/deprecations",
|
|
6
6
|
"models": [
|
|
7
7
|
{
|
|
@@ -318,7 +318,13 @@
|
|
|
318
318
|
},
|
|
319
319
|
{
|
|
320
320
|
"id": "gemini-embedding-2",
|
|
321
|
-
"source": "https://ai.google.dev/gemini-api/docs/deprecations"
|
|
321
|
+
"source": "https://ai.google.dev/gemini-api/docs/deprecations",
|
|
322
|
+
"distributions": [
|
|
323
|
+
{
|
|
324
|
+
"via": "vertex-ai",
|
|
325
|
+
"source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions"
|
|
326
|
+
}
|
|
327
|
+
]
|
|
322
328
|
},
|
|
323
329
|
{
|
|
324
330
|
"id": "gemini-embedding-001",
|
|
@@ -518,9 +524,7 @@
|
|
|
518
524
|
"source": "https://ai.google.dev/gemini-api/docs/deprecations",
|
|
519
525
|
"shutdown": "2026-08-31",
|
|
520
526
|
"date_precision": "earliest",
|
|
521
|
-
"
|
|
522
|
-
"gemini-robotics-er-2-preview"
|
|
523
|
-
]
|
|
527
|
+
"replacement": "gemini-robotics-er-2-preview"
|
|
524
528
|
},
|
|
525
529
|
{
|
|
526
530
|
"id": "gemini-robotics-er-1.5-preview",
|
|
@@ -528,6 +532,85 @@
|
|
|
528
532
|
"shutdown": "2026-04-30",
|
|
529
533
|
"date_precision": "earliest",
|
|
530
534
|
"replacement": "gemini-robotics-er-1.6-preview"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"id": "gemini-3.7-flash",
|
|
538
|
+
"source": "https://ai.google.dev/gemini-api/docs/deprecations",
|
|
539
|
+
"distributions": [
|
|
540
|
+
{
|
|
541
|
+
"via": "vertex-ai",
|
|
542
|
+
"source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions"
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"id": "gemma-4-26b-a4b-it"
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"id": "gemma-4-31b-it"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"id": "gemini-flash-latest"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"id": "gemini-flash-lite-latest"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"id": "gemini-pro-latest"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"id": "gemini-3.1-pro-preview-customtools"
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
"id": "nano-banana-pro-preview"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"id": "gemini-3.1-flash-lite-image",
|
|
569
|
+
"distributions": [
|
|
570
|
+
{
|
|
571
|
+
"via": "vertex-ai",
|
|
572
|
+
"source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions"
|
|
573
|
+
}
|
|
574
|
+
]
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"id": "gemini-omni-flash-preview"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"id": "gemini-robotics-er-2-preview"
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"id": "gemini-2.5-computer-use-preview-10-2025"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"id": "antigravity-preview-05-2026"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
"id": "deep-research-max-preview-04-2026"
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
"id": "deep-research-preview-04-2026"
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"id": "deep-research-pro-preview-12-2025"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
"id": "gemini-embedding-2-preview"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"id": "aqa"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
"id": "gemini-2.5-flash-native-audio-latest"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"id": "gemini-2.5-flash-native-audio-preview-09-2025"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
"id": "gemini-robotics-er-2-streaming-preview"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
"id": "gemini-3.5-live-translate-preview"
|
|
531
614
|
}
|
|
532
615
|
]
|
|
533
616
|
}
|