@aidriftline/driftline 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aidriftline/driftline",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Finds OpenAI, Anthropic and Google model IDs in your code and warns before they are retired. CLI, GitHub Action and MCP server.",
5
5
  "mcpName": "io.github.AI-Driftline/driftline",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
- "checked": "2026-11-14",
2
+ "checked": "2026-11-13",
3
3
  "note": "Hand-maintained from the official deprecation pages. Re-check before each release. Entries with a 'kind' other than text are scanned and reported but never drift-tested. gpt-4-1106-preview is listed on OpenAI's page under both 2026-03-26 and 2026-10-23; the later date is used so builds are not failed on a conflicting record. Entries with routes: true keep answering after the date, but the provider sends the requests to the replacement model (Google calls this automatic routing); the 'retires' date is the day routing starts. gemini-3.5-flash and gemini-3.7-flash already route to newer models with no date given, so they are left out. gemini-3.1-flash-live-preview was removed on 2026-11-14: Google now lists it with no shutdown date.",
4
4
  "sources": {
5
5
  "openai": "https://developers.openai.com/api/docs/deprecations",
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // GitHub Action entry point.
3
- import { replacementText, replacementChange, loadBestRegistry } from "./registry.js";
3
+ import { replacementText, replacementChange, loadBestRegistry, reviewText } from "./registry.js";
4
4
  import { migrationFor } from "./migration.js";
5
5
  import { resolve } from "node:path";
6
6
  import { statSync } from "node:fs";
@@ -65,7 +65,8 @@ async function main() {
65
65
  ? `${f.id} is past its earliest shutdown date (${f.entry.retires}) and may stop working at any time.`
66
66
  : `${f.id} ${STATUS_TEXT[f.status]} (${f.entry.retires}).`;
67
67
  const routedMore = f.entry.routes && migrationFor(f.entry)?.change === "params" ? ` Before then, apply the request changes the provider lists: ${migrationFor(f.entry).note}` : "";
68
- annotate(level, text + repl + more + routedMore, f.file, f.line);
68
+ const review = f.entry.under_review ? ` ${reviewText(f.entry)}` : "";
69
+ annotate(level, text + repl + more + routedMore + review, f.file, f.line);
69
70
  }
70
71
 
71
72
  // The report contains model answers; stop GitHub from reading any "::command::" lines inside it.
package/src/mcp.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { readFileSync, statSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
- import { loadBestRegistry, classify, normalizeId, replacementChange, replacementText } from "./registry.js";
9
+ import { loadBestRegistry, classify, normalizeId, replacementChange, replacementText, reviewText } from "./registry.js";
10
10
  import { migrationFor } from "./migration.js";
11
11
  import { scanDirectory } from "./scan.js";
12
12
 
@@ -47,7 +47,8 @@ function describe(rawId, reg, today) {
47
47
  ...(m.previous_replacement ? { previous_replacement: m.previous_replacement, replacement_changed: m.replacement_changed } : {}), routes: !!m.routes, ...(m.aliasOf ? { alias_of: m.aliasOf } : {}), page: page(m.aliasOf || m.id),
48
48
  } : {}),
49
49
  ...(m && migrationFor(m) ? { migration: migrationFor(m) } : {}),
50
- advice: advice(c, today),
50
+ ...(m?.under_review ? { under_review: m.under_review } : {}),
51
+ advice: (m?.under_review ? reviewText(m) + " " : "") + advice(c, today),
51
52
  };
52
53
  }
53
54
 
@@ -62,6 +63,7 @@ const MODEL_RESULT = {
62
63
  qualifier: { type: "string", enum: ["exact", "earliest", "not-before"], description: "exact date, earliest possible shutdown (Google), or not before this date (Anthropic)" },
63
64
  days_left: { type: "number", description: "Days until the date; 0 or negative when it has passed" },
64
65
  replacement: { type: ["string", "null"] },
66
+ under_review: { type: "object", description: "Present when the provider's page no longer matches this entry and Driftline is updating it: since (date), what (date, replacement or listing), page (source to check)", properties: { since: { type: "string" }, what: { type: "string" }, page: { type: "string" } } },
65
67
  replacement_alt: { type: "string", description: "A second replacement the provider names (\"X or Y\"); which one fits depends on how the model is used" },
66
68
  previous_replacement: { type: "string", description: "The replacement the provider recommended before it changed its advice" },
67
69
  replacement_changed: { type: "string", description: "YYYY-MM-DD: when the change was seen on the provider's page" },
package/src/registry.js CHANGED
@@ -103,6 +103,15 @@ export const isTextModel = (entry) => !entry?.kind || entry.kind === "text";
103
103
  export const replacementText = (entry) => entry?.replacement
104
104
  ? entry.replacement + (entry.replacement_alt ? ` or ${entry.replacement_alt}` : "") + (entry.replacement_note ? ` (${entry.replacement_note})` : "") : "";
105
105
 
106
+ // The live list marks an entry under_review when the provider's page no longer matches it (found by the page check
107
+ // every 6 hours) and the registry hasn't been fixed yet. Say so instead of presenting the stored date as fact.
108
+ const WHO_NAME = { openai: "OpenAI", anthropic: "Anthropic", google: "Google" };
109
+ export function reviewText(entry) {
110
+ const r = entry?.under_review;
111
+ if (!r) return "";
112
+ return `Under review: ${WHO_NAME[entry.provider] || entry.provider}'s page no longer matches the ${r.what || "entry"} shown here (since ${r.since}); Driftline is updating it. Check ${r.page || "the provider's deprecation page"} before acting on this ${r.what === "replacement" ? "replacement" : "date"}.`;
113
+ }
114
+
106
115
  // Providers sometimes change the recommended replacement after announcing a retirement. The registry keeps
107
116
  // the old one and the date we saw the change, so reports can say so instead of silently switching targets.
108
117
  export const RECENT_CHANGE_DAYS = 120;
package/src/report.js CHANGED
@@ -1,4 +1,4 @@
1
- import { replacementChange } from "./registry.js";
1
+ import { replacementChange, reviewText } from "./registry.js";
2
2
  import { migrationFor, MIGRATION_LABEL } from "./migration.js";
3
3
 
4
4
  const STATUS_LABEL = {
@@ -68,6 +68,7 @@ export function renderReport({ findings, drift, registry, notices = [], answers
68
68
  : `Found ${byModel.size} model ID${byModel.size === 1 ? "" : "s"}. None are scheduled for retirement.`);
69
69
  lines.push("", "| Model | Status | Retirement | Replacement | Change needed | Where |", "|---|---|---|---|---|---|");
70
70
  const migNotes = new Map(); // note → { n, ids, guide } for the list under the table
71
+ const underReview = []; // rows whose provider page changed and the registry isn't fixed yet
71
72
  const order = ["retired", "at-risk", "deprecated", "active", "unknown"];
72
73
  const all = [...byModel.values()].sort((a, b) => order.indexOf(a.status) - order.indexOf(b.status));
73
74
  const rows = all.slice(0, MAX_ROWS); // flagged models sort first, so only unknown/active rows get cut
@@ -83,9 +84,14 @@ export function renderReport({ findings, drift, registry, notices = [], answers
83
84
  }
84
85
  const places = r.places.slice(0, 3).join(", ") + (r.places.length > 3 ? ` +${r.places.length - 3} more` : "");
85
86
  const docsOnly = r.places.length && byModel.get(r.id).docs === r.places.length ? " · docs only" : "";
86
- lines.push(`| ${code(r.id)}${r.entry?.aliasOf ? " · alias of " + code(r.entry.aliasOf) : ""}${r.entry?.kind && r.entry.kind !== "text" ? " · " + r.entry.kind : ""} | ${label(r)}${docsOnly} | ${esc(when(r))} | ${r.entry?.replacement ? code(r.entry.replacement) + (r.entry.replacement_alt ? " or " + code(r.entry.replacement_alt) : "") + (r.entry.replacement_note ? " (" + esc(r.entry.replacement_note) + ")" : "") + changed(r.entry, today) : ""} | ${changeCell} | ${places} |`);
87
+ if (r.entry?.under_review) underReview.push(r);
88
+ lines.push(`| ${code(r.id)}${r.entry?.aliasOf ? " · alias of " + code(r.entry.aliasOf) : ""}${r.entry?.kind && r.entry.kind !== "text" ? " · " + r.entry.kind : ""} | ${label(r)}${docsOnly}${r.entry?.under_review ? " · 🔍 under review" : ""} | ${esc(when(r))} | ${r.entry?.replacement ? code(r.entry.replacement) + (r.entry.replacement_alt ? " or " + code(r.entry.replacement_alt) : "") + (r.entry.replacement_note ? " (" + esc(r.entry.replacement_note) + ")" : "") + changed(r.entry, today) : ""} | ${changeCell} | ${places} |`);
87
89
  }
88
90
  if (all.length > rows.length) lines.push("", `_${all.length - rows.length} more model ID(s) with no retirement listed are not shown._`);
91
+ if (underReview.length) {
92
+ lines.push("", "**🔍 Under review:**", "");
93
+ for (const r of underReview) lines.push(`- ${code(r.id)}: ${esc(reviewText(r.entry))}`);
94
+ }
89
95
  if (migNotes.size) {
90
96
  lines.push("", "**More than a rename:**", "");
91
97
  for (const [note, m] of migNotes) lines.push(`${m.n}. ${m.ids.map(code).join(", ")}: ${esc(note)}${m.guide ? ` ([provider guide](${m.guide}))` : ""}`);