parche-cli 0.6.5 → 0.6.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  Todas las versiones de `parche-cli` (npm) y de la Action `Pepo08/parche@v0`.
4
4
 
5
+ ## 0.6.6 — 2026-10-20
6
+
7
+ Pack de IA con el primer cambio de Anthropic que no es un retiro de modelo: desde Claude 4.7, `temperature`, `top_p` y `top_k` con un valor que no es el de por defecto dan error 400 (página de deprecaciones de Anthropic, sección "API parameter deprecations", verificada el 20/10; los valores aceptados, de la referencia de la API y de las guías de migración). Como siempre, Parche avisa y no toca el código.
8
+
9
+ ### Nuevo
10
+ - **Parámetros de muestreo en Claude 4.7 y posteriores:** en `claude-opus-4-7`, `claude-opus-4-8`, `claude-opus-5`, `claude-opus-5-5`, `claude-sonnet-5`, `claude-sonnet-5-5`, `claude-haiku-5-5`, `claude-fable-5`, `claude-fable-5-1`, `claude-mythos-5`, `claude-mythos-5-1` y `claude-mythos-preview`, avisa `temperature` distinto de 1, `top_p` por debajo de 0.99 y `top_k` con cualquier valor. En Claude Haiku 5.5, que es más estricto, también `top_p` distinto de 0.99 (incluso 1) y `temperature` y `top_p` juntos. El aviso dice el parámetro, el modelo, qué acepta y la recomendación oficial: sacar el parámetro y guiar el comportamiento del modelo con el prompt. Con `temperature` en 0 aclara que, según Anthropic, eso nunca garantizó respuestas idénticas; en Python, que el SDK 1.0 o posterior ya no tiene esos parámetros (pasarlos tira `TypeError`).
11
+ - **Dónde lo busca:** en lo que recibe `messages.create` y `messages.stream` del SDK de JS/TS y del de Python (también `beta.`, `with_raw_response.` y `create(params)` / `create(**params)` con el objeto en el mismo archivo), en objetos tipados como `MessageCreateParams` y en el cuerpo de las llamadas HTTP directas a `api.anthropic.com/v1/messages` (fetch, axios, requests). El modelo y el parámetro pueden estar en líneas distintas del pedido.
12
+ - **El modelo, sin inventar:** se lee si es un literal, una constante asignada una sola vez en el archivo o el valor por defecto de una variable (`process.env.CLAUDE_MODEL || '…'`, `os.getenv("CLAUDE_MODEL", "…")`), y el aviso dice cuál de los dos es. Si el modelo no se puede saber (llega de otro archivo, de la configuración o se asigna más de una vez), no avisa.
13
+ - **Sin falsos positivos con otros modelos y proveedores:** Claude Sonnet 4.6, Opus 4.6, Haiku 4.5 y anteriores aceptan los tres parámetros; OpenAI, Gemini y la capa de Anthropic compatible con el SDK de OpenAI no se avisan.
14
+
15
+ ### Todavía no cubre
16
+ - La versión instalada del SDK de Python: con la 1.0 o posterior, pasar `temperature`, `top_p` o `top_k` tira `TypeError` con cualquier valor y cualquier modelo, pero hoy solo se dice en los avisos de Claude 4.7 y posteriores.
17
+ - Frameworks que arman el pedido por su cuenta (LangChain, Vercel AI SDK, LiteLLM), la Message Batches API y los IDs de modelo de Amazon Bedrock (`anthropic.claude-…`).
18
+ - Los SDKs de Go, Java, .NET, Ruby y PHP, y los cuerpos de pedido escritos como texto JSON dentro de un string.
19
+
5
20
  ## 0.6.5 — 2026-10-19
6
21
 
7
22
  Pack de IA con la Videos API de OpenAI, que cerró el 24/9/2026 junto con los modelos Sora 2 (página de deprecaciones verificada el 19/10; los métodos salen de la referencia oficial de la API y del SDK de Python). Hasta ahora solo se avisaban los modelos `sora-2*`; ahora también las llamadas que quedaron en el código sin un ID de modelo cerca (consultar el estado, bajar el video, listar, borrar). Como siempre, Parche avisa y no toca el código.
package/README.md CHANGED
@@ -20,7 +20,7 @@ APIs del repo (6):
20
20
  👀 revisar src/facturacion.js:56 Agregado CondicionIVAReceptorId: 5 por inferencia (DNI + comprobante B/C)…
21
21
  ✋ falta un dato src/facturacion.js:62 Falta CondicionIVAReceptorId y no se puede inferir… (comprobante clase A).
22
22
 
23
- ■ Modelos y endpoints de IA retirados o por retirarse
23
+ ■ Modelos, endpoints y parámetros de IA retirados o por retirarse
24
24
  🔎 detectado src/ia.js:9 El modelo `claude-3-5-haiku-20241022` fue retirado el 2026-02-19: las llamadas ya fallan…
25
25
  …
26
26
  ```
@@ -131,7 +131,7 @@ Con el tag, el workflow corre los tests, controla que la versión de `package.js
131
131
  |---|---|---|---|---|
132
132
  | `arca.wsfe.condicion-iva-receptor` | ARCA | rompe | 2026-12-01 | sí, en JS (`afip.js`) y PHP (`afipsdk/afip.php` y forks): consumidor final y DNI B/C; con `DocTipo` en una variable, consumidor final al ejecutarse (PR parcial) |
133
133
  | `mercadopago.payments-to-orders` | Mercado Pago | migración | — | sí (creación de pagos en SDK Node v2) |
134
- | `ai.model-retirements` | Anthropic, OpenAI | retiro | varias | solo detección (también modelos fine-tuned de OpenAI, `ft:…`). De OpenAI, los modelos que su página de deprecaciones apaga desde 2026: texto, código, imagen, video, voz, transcripción y realtime; y los productos que cierran: Assistants API, prompts reutilizables (`pmpt_…`, `v1/prompts`), la plataforma de Evals, ChatKit con un workflow de Agent Builder y la Videos API (cerrada el 24/9/2026) |
134
+ | `ai.model-retirements` | Anthropic, OpenAI | retiro | varias | solo detección (también modelos fine-tuned de OpenAI, `ft:…`). De OpenAI, los modelos que su página de deprecaciones apaga desde 2026: texto, código, imagen, video, voz, transcripción y realtime; y los productos que cierran: Assistants API, prompts reutilizables (`pmpt_…`, `v1/prompts`), la plataforma de Evals, ChatKit con un workflow de Agent Builder y la Videos API (cerrada el 24/9/2026). De Anthropic, los modelos retirados y `temperature`, `top_p` y `top_k` con un valor que no es el de por defecto en Claude 4.7 y posteriores, que dan error 400 (SDK de JS/TS y de Python, y HTTP directo) |
135
135
 
136
136
  ## Verificación
137
137
 
@@ -188,6 +188,7 @@ Cuando `DocTipo` no es un número fijo (`DocTipo: docTipo`), Parche no puede sab
188
188
 
189
189
  - JS/TS (ESM y CommonJS) y, para ARCA, PHP. Siguen Python y Java.
190
190
  - ARCA: cubre `@afipsdk/afip.js` y `afipsdk/afip.php` (con sus forks). Las integraciones SOAP propias (en cualquier lenguaje) se detectan pero no se reescriben.
191
+ - IA, parámetros de Claude 4.7 y posteriores: avisa solo si el modelo del pedido se lee en el mismo archivo (un literal, una constante asignada una vez o el valor por defecto de una variable de entorno) y el parámetro es un número escrito en el código. Si el modelo llega de otro archivo o de la configuración, no avisa: no se puede saber si el pedido va a fallar.
191
192
  - Mercado Pago: migra la creación de pagos con el SDK v2. Las consultas, capturas y reembolsos de pagos existentes se dejan a propósito en Payments API, como indica la guía oficial.
192
193
  - Se distribuye como CLI (`npx parche-cli`) + GitHub Action (`Pepo08/parche@v0`). La GitHub App (instalación en un click, PRs automáticos al publicarse un pack nuevo) es el próximo paso.
193
194
  - La capa de LLM (redactar packs desde changelogs y resolver casos "falta un dato" leyendo el modelo de datos del cliente) todavía no está incluida; cuando entre, va a pasar por la misma verificación.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Dependabot para APIs: detecta cambios de APIs de terceros en tu código y abre el PR con el arreglo.",
5
5
  "keywords": [
6
6
  "api",
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "id": "ai.model-retirements",
3
- "version": "2026.10.19",
3
+ "version": "2026.10.20",
4
4
  "provider": "Modelos de IA",
5
- "title": "Modelos y endpoints de IA retirados o por retirarse",
5
+ "title": "Modelos, endpoints y parámetros de IA retirados o por retirarse",
6
6
  "kind": "retirement",
7
7
  "severity": "high",
8
8
  "deadline": null,
9
9
  "last_verified": "2026-10-19",
10
- "summary_es": "Tu código usa modelos o APIs de IA que ya no responden o tienen fecha de retiro.",
11
- "why_es": "Los proveedores de modelos retiran IDs de modelo y endpoints con algunas semanas o meses de aviso. Después de la fecha, las llamadas fallan. Parche no reemplaza el modelo automáticamente porque cambiar de modelo cambia el comportamiento: te avisa, te dice la fecha y el reemplazo que recomienda el proveedor.",
10
+ "summary_es": "Tu código usa modelos, APIs o parámetros de IA que ya no responden, tienen fecha de retiro o el modelo nuevo rechaza.",
11
+ "why_es": "Los proveedores de modelos retiran IDs de modelo y endpoints con algunas semanas o meses de aviso. Después de la fecha, las llamadas fallan. Parche no reemplaza el modelo automáticamente porque cambiar de modelo cambia el comportamiento: te avisa, te dice la fecha y el reemplazo que recomienda el proveedor. También avisa los parámetros que los modelos nuevos rechazan, como `temperature`, `top_p` y `top_k` en Claude 4.7 y posteriores.",
12
12
  "sources": [
13
13
  { "title": "Anthropic: Model deprecations", "url": "https://platform.claude.com/docs/en/about-claude/model-deprecations" },
14
+ { "title": "Anthropic: Model deprecations, API parameter deprecations (temperature, top_p y top_k en Claude 4.7 y posteriores)", "url": "https://platform.claude.com/docs/en/about-claude/model-deprecations#api-parameter-deprecations" },
15
+ { "title": "Anthropic: API reference, Messages (valores que se aceptan: temperature 1, top_p desde 0.99, top_k ninguno)", "url": "https://platform.claude.com/docs/en/api/messages/create" },
16
+ { "title": "Anthropic: Claude Haiku 5.5 migration guide (top_p solo 0.99; temperature y top_p juntos dan 400)", "url": "https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide#remove-sampling-parameters" },
17
+ { "title": "Anthropic: Claude Opus 5.5 migration guide (Opus 4.7 y posteriores, Sonnet 5; SDK de Python 1.0 y TypeError)", "url": "https://platform.claude.com/docs/en/models/opus-5-5/migration-guide" },
18
+ { "title": "Anthropic: Claude Sonnet 5.5 migration guide (Sonnet 4.6 y anteriores y Haiku 4.5 aceptan los tres)", "url": "https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide" },
19
+ { "title": "Anthropic: Claude Fable 5.1 and Claude Mythos 5.1 migration guide (la restricción de muestreo se mantiene)", "url": "https://platform.claude.com/docs/en/models/fable-5-1/migration-guide" },
14
20
  { "title": "OpenAI: Deprecations", "url": "https://developers.openai.com/api/docs/deprecations" },
15
21
  { "title": "OpenAI: Supervised fine-tuning (formato de los IDs ft:)", "url": "https://developers.openai.com/api/docs/guides/supervised-fine-tuning" },
16
22
  { "title": "OpenAI: Models (snapshots de cada familia y de cada modelo base)", "url": "https://developers.openai.com/api/docs/models" },
@@ -163,6 +169,28 @@
163
169
  "claude-haiku-4-5-20251001": "2026-10-15",
164
170
  "claude-opus-4-5-20251101": "2026-11-24"
165
171
  },
172
+ "sampling_params": {
173
+ "last_verified": "2026-10-20",
174
+ "models": ["claude-opus-4-7", "claude-opus-4-8", "claude-opus-5", "claude-opus-5-5", "claude-sonnet-5", "claude-sonnet-5-5", "claude-haiku-5-5",
175
+ "claude-fable-5", "claude-fable-5-1", "claude-mythos-5", "claude-mythos-5-1", "claude-mythos-preview"],
176
+ "scope_es": "en Claude 4.7 y posteriores (y en Claude Mythos Preview)",
177
+ "accepted": {
178
+ "temperature": { "min": 1, "max": 1, "es": "solo acepta 1, su valor por defecto" },
179
+ "top_p": { "min": 0.99, "max": 1, "es": "solo acepta valores desde 0.99" },
180
+ "top_k": { "es": "no acepta ningún valor" }
181
+ },
182
+ "overrides": {
183
+ "claude-haiku-5-5": {
184
+ "scope_es": "en Claude Haiku 5.5",
185
+ "accepted": { "top_p": { "min": 0.99, "max": 0.99, "es": "solo acepta 0.99, su valor por defecto (ni siquiera 1)" } },
186
+ "exclusive": ["temperature", "top_p"],
187
+ "exclusive_es": "Claude Haiku 5.5 rechaza los pedidos que mandan los dos, aunque tengan sus valores por defecto"
188
+ }
189
+ },
190
+ "recommendation_es": "sacar {params} del pedido y guiar el comportamiento del modelo con el prompt",
191
+ "temperature_zero_es": "Si estaba en 0 para tener respuestas determinísticas: según Anthropic, eso nunca garantizó respuestas idénticas.",
192
+ "python_sdk_es": "Además, el SDK de Python 1.0 o posterior ya no tiene `temperature`, `top_p` ni `top_k`: pasarlos tira `TypeError`."
193
+ },
166
194
  "retired_endpoints": [
167
195
  { "pattern": ".beta.assistants", "retired": "2026-08-26", "api_es": "Assistants API", "replacement_es": "Responses API + Conversations API", "provider": "openai" },
168
196
  { "pattern": ".beta.threads", "retired": "2026-08-26", "api_es": "Assistants API", "replacement_es": "Responses API + Conversations API", "provider": "openai" },
@@ -182,6 +210,6 @@
182
210
  "review_checklist_es": [
183
211
  "Elegir el modelo de reemplazo según la página de deprecaciones del proveedor.",
184
212
  "Correr tus evals o pruebas de regresión de prompts antes de cambiar de modelo.",
185
- "Revisar parámetros que cambian entre generaciones de modelos."
213
+ "Revisar parámetros que cambian entre generaciones de modelos (desde Claude 4.7, `temperature`, `top_p` y `top_k`)."
186
214
  ]
187
215
  }
@@ -4,6 +4,8 @@
4
4
  // Menos ruido (frente B, 16/10): un ID que aparece en el contexto de una llamada (model: '...', DEFAULT_MODEL)
5
5
  // es un uso; si aparece en una lista o un catálogo, se avisa como mención. Y en código de Azure OpenAI no se
6
6
  // da por cerrada la Assistants API: la fecha oficial es la de la API de OpenAI.
7
+ import samplingParams from './anthropic-sampling-params.js';
8
+
7
9
  const PACK = 'ai.model-retirements';
8
10
 
9
11
  const MODEL_CONTEXT = /\bmodel(?:Id|Name)?\b\s*[:=(]|["']model["']\s*[:=]|\b(?:DEFAULT|default)_?(?:MODEL|Model)\b|\bmodel\s*\(/i;
@@ -92,5 +94,10 @@ export default function detect({ source, file, pack, today = new Date().toISOStr
92
94
  usages.push({ provider: 'ia', op: `OpenAI ${ep.api_es}`, line });
93
95
  }
94
96
  });
97
+ // `temperature`, `top_p` y `top_k` en Claude 4.7 y posteriores: el modelo y el parámetro pueden estar en líneas
98
+ // distintas del pedido, así que va aparte (anthropic-sampling-params.js).
99
+ const sampling = samplingParams({ source, file, pack });
100
+ findings.push(...sampling.findings);
101
+ usages.push(...sampling.usages);
95
102
  return { output: null, findings, usages };
96
103
  }
@@ -0,0 +1,322 @@
1
+ // Pack ai.model-retirements — `temperature`, `top_p` y `top_k` en Claude 4.7 y posteriores. Solo detección.
2
+ // Desde Claude 4.7 (y en Claude Mythos Preview), la API de Anthropic devuelve 400 si alguno de los tres llega con un
3
+ // valor distinto del de por defecto (página de deprecaciones, "API parameter deprecations"). Los valores aceptados
4
+ // salen de la referencia de la API (`temperature` 1, `top_p` desde 0.99, `top_k` ninguno) y de la guía de Claude
5
+ // Haiku 5.5, que es más estricta (`top_p` solo 0.99, y nunca `temperature` y `top_p` juntos).
6
+ //
7
+ // El modelo y el parámetro suelen estar en líneas distintas del mismo pedido, así que no alcanza con mirar una línea
8
+ // como en los modelos retirados: se tokeniza el archivo (strings y comentarios de JS/TS o de Python), se arma el árbol
9
+ // de paréntesis y llaves, y se mira cada objeto que es un pedido a la API de Messages:
10
+ // 1. lo que recibe `.messages.create(` o `.messages.stream(` (también `beta.` y `with_raw_response.`): el objeto en
11
+ // JS, los argumentos con nombre en Python;
12
+ // 2. el objeto asignado a la variable que recibe esa llamada (`create(params)`, `create(**params)`);
13
+ // 3. un objeto tipado como `MessageCreateParams…`;
14
+ // 4. si el archivo nombra `api.anthropic.com` y `v1/messages` (HTTP directo): cualquier objeto con `messages` o
15
+ // `max_tokens`, salvo los que van a una llamada de OpenAI (`chat.completions.create(`, `responses.create(`).
16
+ // Se avisa solo si el modelo del pedido se puede leer y es uno de los afectados, y si el parámetro es un número fuera
17
+ // de lo aceptado. El modelo se lee si es un literal, una constante asignada una sola vez en el archivo o el valor por
18
+ // defecto de una variable (`process.env.X || '…'`, `os.getenv("X", "…")`). Si no se puede saber, no se avisa (D64).
19
+ const PACK = 'ai.model-retirements';
20
+ const JS = /\.[cm]?[jt]sx?$/i;
21
+ const PY = /\.py$/i;
22
+ const PARAMS = ['temperature', 'top_p', 'top_k'];
23
+ const MESSAGES_CALL = /\.\s*messages\s*\.\s*(?:with_(?:raw|streaming)_response\s*\.\s*)?(?:create|stream)\s*$/;
24
+ const OPENAI_CALL = /\.\s*(?:chat\s*\.\s*)?completions\s*\.\s*create\s*$|\.\s*responses\s*\.\s*(?:create|stream|parse)\s*$/;
25
+ const TYPED_PARAMS = /MessageCreateParams\w*(?:<[^>]*>)?\s*=\s*$/;
26
+ const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
27
+ const IDENT = /^[A-Za-z_$][\w$]*$/;
28
+ const NAMES = /[A-Za-z_$][\w$]*/g;
29
+ // Antes de una `/` que abre una regex de JS (y no es una división) hay un operador, un separador o una palabra clave.
30
+ const REGEX_BEFORE = /(?:^|[(,=:[!&|?{};+\-*%<>~^]|\b(?:return|typeof|case|in|of|yield|await|void|delete|throw|else|do))\s*$/;
31
+ const CONTROL = /\b(?:if|for|while|switch|catch|with)\s*$/;
32
+
33
+ // Copia del código con los comentarios y el interior de los strings en blanco (mismas posiciones y saltos de línea),
34
+ // para buscar llaves, comas y claves sin tropezar con texto. Los strings quedan aparte, con su valor.
35
+ function tokenize(source, python) {
36
+ const code = source.split('');
37
+ const strings = [];
38
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (code[k] !== '\n' && code[k] !== '\r') code[k] = ' '; };
39
+ const n = source.length;
40
+ let i = 0;
41
+ while (i < n) {
42
+ const c = source[i];
43
+ if (python ? c === '#' : source.startsWith('//', i)) {
44
+ const e = source.indexOf('\n', i);
45
+ const end = e < 0 ? n : e;
46
+ blank(i, end); i = end; continue;
47
+ }
48
+ if (!python && source.startsWith('/*', i)) {
49
+ const e = source.indexOf('*/', i + 2);
50
+ const end = e < 0 ? n : e + 2;
51
+ blank(i, end); i = end; continue;
52
+ }
53
+ if (c === '"' || c === "'" || (c === '`' && !python)) {
54
+ const q = python && source.startsWith(c.repeat(3), i) ? c.repeat(3) : c;
55
+ let j = i + q.length;
56
+ while (j < n && !source.startsWith(q, j)) {
57
+ if (source[j] === '\\') j++;
58
+ else if (source[j] === '\n' && q.length === 1 && c !== '`') break; // string sin cerrar: termina en la línea
59
+ j++;
60
+ }
61
+ const end = Math.min(n, j + q.length);
62
+ strings.push({ start: i, end, value: source.slice(i + q.length, j) });
63
+ blank(i + q.length, j);
64
+ i = end; continue;
65
+ }
66
+ if (!python && c === '/' && REGEX_BEFORE.test(code.slice(Math.max(0, i - 12), i).join(''))) {
67
+ let j = i + 1;
68
+ let inClass = false;
69
+ while (j < n && source[j] !== '\n' && (inClass || source[j] !== '/')) {
70
+ if (source[j] === '\\') j++;
71
+ else if (source[j] === '[') inClass = true;
72
+ else if (source[j] === ']') inClass = false;
73
+ j++;
74
+ }
75
+ blank(i + 1, j);
76
+ i = j + 1; continue;
77
+ }
78
+ i++;
79
+ }
80
+ return { code: code.join(''), strings };
81
+ }
82
+
83
+ // Árbol de paréntesis, corchetes y llaves, y la profundidad de cada posición. Un cierre que no corresponde al último
84
+ // abierto cierra hasta su par (código a medio escribir o una regex que no se reconoció); si no tiene par, se ignora.
85
+ function brackets(code) {
86
+ const PAIR = { ')': '(', ']': '[', '}': '{' };
87
+ const nodes = [];
88
+ const stack = [];
89
+ const depth = new Uint16Array(code.length + 1);
90
+ for (let i = 0; i < code.length; i++) {
91
+ const c = code[i];
92
+ depth[i] = stack.length;
93
+ if (c === '(' || c === '[' || c === '{') {
94
+ const node = { open: c, start: i, end: -1, parent: stack.at(-1) || null };
95
+ stack.push(node);
96
+ nodes.push(node);
97
+ } else if (PAIR[c]) {
98
+ const k = stack.map((x) => x.open).lastIndexOf(PAIR[c]);
99
+ if (k < 0) continue;
100
+ for (const x of stack.splice(k)) x.end = i;
101
+ }
102
+ }
103
+ return { nodes: nodes.filter((x) => x.end >= 0), depth };
104
+ }
105
+
106
+ // Primer separador de primer nivel en code[a, b): `:` en objetos y diccionarios, `=` en argumentos con nombre de Python
107
+ // (no `==`, `<=`, `>=`, `!=`).
108
+ function separator(code, a, b, sep) {
109
+ let level = 0;
110
+ for (let i = a; i < b; i++) {
111
+ const c = code[i];
112
+ if ('([{'.includes(c)) level++;
113
+ else if (')]}'.includes(c)) level--;
114
+ else if (level === 0 && c === sep) {
115
+ if (sep === '=' && (code[i + 1] === '=' || '=!<>'.includes(code[i - 1]))) continue;
116
+ return i;
117
+ }
118
+ }
119
+ return -1;
120
+ }
121
+
122
+ export default function detectSamplingParams({ source, file, pack }) {
123
+ const rules = pack.detect.sampling_params;
124
+ const python = PY.test(file);
125
+ if (!rules || (!python && !JS.test(file))) return { findings: [], usages: [] };
126
+ // La mayoría de los archivos no tiene ni el parámetro ni un modelo afectado: no hace falta tokenizarlos.
127
+ if (!PARAMS.some((p) => source.includes(p)) || !rules.models.some((m) => source.includes(m))) return { findings: [], usages: [] };
128
+
129
+ const { code, strings } = tokenize(source, python);
130
+ const { nodes, depth } = brackets(code);
131
+ const stringAt = new Map(strings.map((s) => [s.start, s]));
132
+ const lineStarts = [0];
133
+ for (let i = 0; i < source.length; i++) if (source[i] === '\n') lineStarts.push(i + 1);
134
+ const lineOf = (pos) => {
135
+ let lo = 0;
136
+ let hi = lineStarts.length - 1;
137
+ while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (lineStarts[mid] <= pos) lo = mid; else hi = mid - 1; }
138
+ return lo + 1;
139
+ };
140
+ // Texto de code[a, b) sin los espacios (ni los comentarios) de los bordes, tomado del original. Sin `as const` ni
141
+ // `satisfies T` de TypeScript al final.
142
+ const trimmed = (a, b) => {
143
+ while (a < b && /\s/.test(code[a])) a++;
144
+ while (b > a && /\s/.test(code[b - 1])) b--;
145
+ const cast = python ? null : code.slice(a, b).match(/\s+(?:as|satisfies)\s+[\w$.<>\[\]]+$/);
146
+ if (cast) b -= cast[0].length;
147
+ return { at: a, end: b, text: source.slice(a, b) };
148
+ };
149
+ const literal = (v) => { const s = stringAt.get(v.at); return s && s.end === v.end ? s.value : null; };
150
+ const before = (node) => code.slice(Math.max(0, node.start - 160), node.start);
151
+ const nodeAt = (pos) => nodes.find((x) => x.start === pos) || null;
152
+
153
+ // Asignaciones del archivo. Una variable cuenta como constante si se asigna una sola vez, en una declaración
154
+ // (`const X = …` en JS; `X = …` fuera de paréntesis en Python) y con el valor en la misma línea. Todo lo demás que
155
+ // le da un valor a un nombre (parámetros de funciones, desestructuración, imports, `for`, `+=`) cuenta como otra
156
+ // asignación: así un parámetro que se llama igual que una constante no hace leer un valor que no es.
157
+ let assigned = null;
158
+ const assignments = () => {
159
+ if (assigned) return assigned;
160
+ assigned = new Map();
161
+ const add = (name, at, valueAt) => {
162
+ const list = assigned.get(name) || [];
163
+ if (!list.some((x) => x.at === at)) list.push({ at, valueAt });
164
+ assigned.set(name, list);
165
+ };
166
+ const bind = (a, b) => { for (const m of code.slice(a, b).matchAll(NAMES)) add(m[0], a + m.index, null); };
167
+ if (python) {
168
+ for (const m of code.matchAll(/^[ \t]*([A-Za-z_]\w*)[ \t]*(?::[^=\n]*)?=(?!=)/gm)) {
169
+ const at = m.index + m[0].indexOf(m[1]);
170
+ if (depth[at] === 0) add(m[1], at, m.index + m[0].length);
171
+ }
172
+ for (const m of code.matchAll(/^[ \t]*([A-Za-z_]\w*)[ \t]*(?:[-+*/%@&|^]|\/\/|\*\*|>>|<<)=/gm)) add(m[1], m.index + m[0].indexOf(m[1]), null);
173
+ for (const m of code.matchAll(/^[ \t]*([\w \t*]+,[\w \t*,]*)=(?!=)/gm)) bind(m.index, m.index + m[1].length + m[0].indexOf(m[1]));
174
+ for (const m of code.matchAll(/\b(?:for|as|global|nonlocal)\s+([A-Za-z_]\w*)\b|\b([A-Za-z_]\w*)\s*:=/g)) add(m[1] || m[2], m.index + m[0].indexOf(m[1] || m[2]), null);
175
+ for (const m of code.matchAll(/^[ \t]*(?:from\s+\S+\s+)?import\s+([^\n]+)/gm)) bind(m.index + m[0].length - m[1].length, m.index + m[0].length);
176
+ for (const x of nodes) if (x.open === '(' && /\bdef\s+\w+\s*$/.test(before(x))) bind(x.start + 1, x.end);
177
+ } else {
178
+ for (const m of code.matchAll(/(?<![\w$.])(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]*)?=(?![=>])/g)) {
179
+ add(m[1], m.index + m[0].indexOf(m[1], m[0].search(/\s/)), m.index + m[0].length);
180
+ }
181
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:[-+*/%&|^]|\*\*|\?\?|\|\||&&|>>>?|<<)?=(?![=>])/g)) add(m[1], m.index, null);
182
+ for (const m of code.matchAll(/(?<![\w$.])(?:const|let|var)\s*([{[])/g)) {
183
+ const x = nodeAt(m.index + m[0].length - 1);
184
+ if (x) bind(x.start, x.end);
185
+ }
186
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*=>/g)) add(m[1], m.index, null);
187
+ for (const m of code.matchAll(/^[ \t]*import\s+([^;\n]*?)\s+from\b/gm)) bind(m.index, m.index + m[0].length);
188
+ for (const x of nodes) {
189
+ if (x.open === '(' && !CONTROL.test(before(x)) && /^\s*(?::[^={;]*)?(?:=>|\{)/.test(code.slice(x.end + 1, x.end + 120))) bind(x.start + 1, x.end);
190
+ }
191
+ }
192
+ const lineEnd = (i) => { const e = code.indexOf('\n', i); return e < 0 ? code.length : e; };
193
+ for (const list of assigned.values()) {
194
+ for (const x of list) {
195
+ if (x.valueAt === null) continue;
196
+ let { at, end } = trimmed(x.valueAt, lineEnd(x.valueAt));
197
+ while (end > at && (code[end - 1] === ';' || code[end - 1] === ',' || /\s/.test(code[end - 1]))) end--;
198
+ x.value = trimmed(at, end);
199
+ }
200
+ }
201
+ return assigned;
202
+ };
203
+ const constant = (name) => {
204
+ const list = assignments().get(name) || [];
205
+ return list.length === 1 && list[0].value ? list[0].value : null;
206
+ };
207
+
208
+ // El modelo de un valor: literal, constante o el valor por defecto de una variable. `from` dice de dónde sale.
209
+ const modelOf = (v, nested = false) => {
210
+ const lit = literal(v);
211
+ if (lit !== null) return { id: lit, from: null };
212
+ const t = v.text;
213
+ if (IDENT.test(t)) { const c = !nested && constant(t); return c ? modelOf(c, true) : null; }
214
+ const js = !python && t.match(/^(process\.env(?:\.\w+|\[\s*(['"])\w+\2\s*\])|[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)*)\s*(?:\|\||\?\?)\s*(['"`])([^'"`\\$]*)\3$/);
215
+ if (js) return { id: js[4], from: `\`${js[1]}\`` };
216
+ const env = python && t.match(/^(?:os\.)?(?:getenv|environ\.get)\(\s*(['"])(\w+)\1\s*,\s*(['"])([^'"\\]*)\3\s*\)$/);
217
+ if (env) return { id: env[4], from: `la variable de entorno \`${env[2]}\`` };
218
+ const or = python && t.match(/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s+or\s+(['"])([^'"\\]*)\2$/);
219
+ if (or) return { id: or[3], from: `\`${or[1]}\`` };
220
+ return null;
221
+ };
222
+ // El número de un valor: literal o constante. `via` es el nombre de la constante.
223
+ const numberOf = (v) => {
224
+ if (NUMBER.test(v.text)) return { value: Number(v.text), via: null };
225
+ const c = IDENT.test(v.text) && constant(v.text);
226
+ return c && NUMBER.test(c.text) ? { value: Number(c.text), via: v.text } : null;
227
+ };
228
+
229
+ // Entradas de primer nivel de un nodo: { key, sep, keyAt, value }. En JS, `clave: valor`, `'clave': valor` y la forma
230
+ // corta `{ temperature }` (sep null); en Python, `"clave": valor` en diccionarios y `clave=valor` en argumentos.
231
+ const entriesOf = (node) => {
232
+ const sep = node.open === '{' ? ':' : '=';
233
+ const out = [];
234
+ const push = (a, b) => {
235
+ const s = separator(code, a, b, sep);
236
+ if (s < 0) {
237
+ const v = trimmed(a, b);
238
+ if (!python && sep === ':' && IDENT.test(v.text)) out.push({ key: v.text, sep: null, keyAt: v.at, value: v });
239
+ return;
240
+ }
241
+ const k = trimmed(a, s);
242
+ const key = IDENT.test(k.text) ? k.text : literal(k);
243
+ if (key !== null) out.push({ key, sep, keyAt: k.at, value: trimmed(s + 1, b) });
244
+ };
245
+ let level = 0;
246
+ let a = node.start + 1;
247
+ for (let i = node.start + 1; i < node.end; i++) {
248
+ const c = code[i];
249
+ if ('([{'.includes(c)) level++;
250
+ else if (')]}'.includes(c)) level--;
251
+ else if (c === ',' && level === 0) { push(a, i); a = i + 1; }
252
+ }
253
+ push(a, node.end);
254
+ return out;
255
+ };
256
+
257
+ // Los pedidos: nodo → 'sdk' o 'http'.
258
+ const requests = new Map();
259
+ const paramVars = new Set();
260
+ for (const call of nodes.filter((x) => x.open === '(' && MESSAGES_CALL.test(before(x)))) {
261
+ const comma = separator(code, call.start + 1, call.end, ',');
262
+ const first = trimmed(call.start + 1, comma < 0 ? call.end : comma);
263
+ if (python) requests.set(call, 'sdk');
264
+ else if (code[first.at] === '{') requests.set(nodeAt(first.at), 'sdk');
265
+ const name = first.text.replace(/^\*\*/, '');
266
+ if (IDENT.test(name)) paramVars.add(name);
267
+ }
268
+ const http = /api\.anthropic\.com/.test(source) && /v1\/messages/.test(source);
269
+ for (const node of nodes) {
270
+ if (node.open !== '{' || requests.has(node)) continue;
271
+ const head = before(node);
272
+ const target = head.match(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?::[^=;\n]*)?=\s*$/);
273
+ if ((target && paramVars.has(target[1])) || TYPED_PARAMS.test(head)) { requests.set(node, 'sdk'); continue; }
274
+ if (!http) continue;
275
+ let openai = false;
276
+ for (let p = node.parent; p && !openai; p = p.parent) openai = p.open === '(' && OPENAI_CALL.test(before(p));
277
+ if (!openai) requests.set(node, 'http');
278
+ }
279
+
280
+ const findings = [];
281
+ const usages = [];
282
+ const report = (at, message, op) => {
283
+ findings.push({ pack: PACK, file, line: lineOf(at), status: 'detect-only', message });
284
+ usages.push({ provider: 'ia', op, line: lineOf(at) });
285
+ };
286
+ for (const [node, via] of requests) {
287
+ if (!node) continue;
288
+ const byKey = new Map(entriesOf(node).map((e) => [e.key, e]));
289
+ if (via === 'http' && !byKey.has('messages') && !byKey.has('max_tokens')) continue;
290
+ const model = byKey.has('model') ? modelOf(byKey.get('model').value) : null;
291
+ if (!model || !rules.models.includes(model.id)) continue;
292
+ const override = rules.overrides?.[model.id] || {};
293
+ const scope = override.scope_es || rules.scope_es;
294
+ const accepted = { ...rules.accepted, ...override.accepted };
295
+ const modelText = `\`${model.id}\`${model.from ? ` (el valor por defecto de ${model.from})` : ''}`;
296
+ const tail = (params, zero) => `Recomendación oficial de Anthropic: ${rules.recommendation_es.replace('{params}', params.map((p) => `\`${p}\``).join(' y '))}.`
297
+ + (zero ? ` ${rules.temperature_zero_es}` : '') + (python && via === 'sdk' ? ` ${rules.python_sdk_es}` : '');
298
+ const present = [];
299
+ let flagged = 0;
300
+ for (const param of PARAMS) {
301
+ const e = byKey.get(param);
302
+ const num = e && numberOf(e.value);
303
+ if (!num) continue;
304
+ present.push({ param, e });
305
+ const ok = accepted[param];
306
+ if (ok.min !== undefined && num.value >= ok.min && num.value <= ok.max) continue;
307
+ flagged++;
308
+ const shown = (e.sep === '=' ? `${param}=${e.value.text}` : e.sep ? `${param}: ${e.value.text}` : param) + (num.via ? ` (${num.value})` : '');
309
+ report(e.keyAt, `\`${shown}\` con el modelo ${modelText} da un error 400: ${scope}, \`${param}\` ${ok.es}. ${tail([param], param === 'temperature' && num.value === 0)}`,
310
+ `Anthropic ${param} (Claude 4.7+)`);
311
+ }
312
+ const together = override.exclusive || [];
313
+ if (together.length && !flagged && together.every((p) => present.some((x) => x.param === p))) {
314
+ const last = present.filter((x) => together.includes(x.param)).at(-1);
315
+ report(last.e.keyAt, `${together.map((p) => `\`${p}\``).join(' y ')} juntos con el modelo ${modelText} dan un error 400: ${override.exclusive_es}. ${tail(together, false)}`,
316
+ `Anthropic ${together.join(' + ')} (Claude 4.7+)`);
317
+ }
318
+ }
319
+ // Los pedidos salen en el orden del árbol (de afuera hacia adentro); el reporte va en el orden del archivo.
320
+ const byLine = (a, b) => a.line - b.line;
321
+ return { findings: findings.sort(byLine), usages: usages.sort(byLine) };
322
+ }