parche-cli 0.6.5 → 0.6.7

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
@@ -1,6 +1,56 @@
1
1
  # Cambios
2
2
 
3
- Todas las versiones de `parche-cli` (npm) y de la Action `Pepo08/parche@v0`.
3
+ Los cambios de cada versión de `parche-cli` y de la Action `Pepo08/parche@v0`. En npm están la 0.1.1, la 0.5.1 y de la 0.6.0 en adelante. La 0.1.0 y de la 0.2.0 a la 0.5.0 no se publicaron en npm; lo que traían está incluido en la 0.5.1. La fecha de cada versión es la del cambio; la de publicación figura en npm y en los [Releases](https://github.com/Pepo08/parche/releases).
4
+
5
+ ## 0.6.7 — 2026-10-21
6
+
7
+ Pack de IA con la API version `2026-10-21` de Anthropic, que salió el 21/10 junto con el SDK de TypeScript 1.0.0 y el de Python 2.0.0: elimina `temperature`, `top_p` y `top_k` para cualquier modelo, y un pedido que los manda devuelve un error 400 aunque tengan su valor por defecto (guía "Migrating to API version 2026-10-21" y página de deprecaciones, verificadas el 21/10). Los SDK anteriores siguen mandando `2023-06-01`, así que hoy casi nada se rompe: se rompe con la próxima suba del SDK. Como siempre, Parche avisa y no toca el código. Esta versión trae además las bases de confianza del repo (al final).
8
+
9
+ ### Nuevo
10
+ - **Los tres parámetros con cualquier modelo,** también Claude Sonnet 4.6, Opus 4.6 y Haiku 4.5, que en `2023-06-01` los aceptan. El aviso dice en cuál de tres casos está el pedido:
11
+ - **ya falla:** el archivo manda `anthropic-version: 2026-10-21`, o el SDK instalado ya es el nuevo (`@anthropic-ai/sdk` 1.0.0 o posterior; `anthropic` 2.0.0 o posterior por `extra_body`). En Python, el parámetro directo ya tira `TypeError` desde la 1.0.0 (MIGRATION.md del SDK);
12
+ - **falla en una instalación nueva:** el rango declarado no tiene tope (`>=0.30.0`, `>=0.40`) y ya deja instalar la versión nueva;
13
+ - **va a fallar con la suba:** el SDK instalado es anterior; el aviso pide sacar el parámetro antes de aceptar esa suba.
14
+ - **Sin repetir avisos:** lo que ya marca la regla de Claude 4.7 (un valor fuera de lo aceptado) sale una sola vez. En un modelo que todavía acepta otros valores, el aviso aclara que sacarlo cambia cómo responde y que la alternativa es dejar ese pedido en `2023-06-01`. Un archivo que fija `anthropic-version: 2023-06-01` no se avisa.
15
+ - **Versión de Python desde `requirements.txt`:** el contexto del repo ahora también trae las versiones declaradas de Python y el rango tal como está escrito.
16
+ - **PRs abiertos con el `GITHUB_TOKEN`:** GitHub deja sus workflows esperando aprobación. Cuando la Action corre con el token por defecto, el cuerpo del PR lo dice y explica cómo usar un token de GitHub App o uno personal para que la CI corra sola (README, "En GitHub Actions").
17
+ - **La Action se llama "Parche API Changes"** (antes, "Parche — Dependabot para APIs"), para publicarla en el GitHub Marketplace. Sus reglas no aceptan un nombre igual a una función de GitHub (Dependabot, que queda solo en la descripción) ni a un usuario de GitHub (existe la cuenta `parche`).
18
+
19
+ ### Cambios
20
+ - La fuente del `TypeError` del SDK de Python pasa a ser su MIGRATION.md de la 1.0.0: la guía de migración a Opus 5.5 ya no lo menciona.
21
+
22
+ ### Todavía no cubre
23
+ - La versión del SDK de Python desde `pyproject.toml`, `poetry.lock` o `uv.lock`, y los rangos sin número (`latest`, `*`), que cuentan como versión desconocida.
24
+ - Los SDKs de Go (v2), Java (3.0.0), .NET (13.0.0), Ruby (2.0.0) y PHP (1.0.0), que también pasan a `2026-10-21`.
25
+ - Los otros cambios de esa API version: bloques `document` en mensajes de `assistant`, `citations` en mensajes de `user` y los campos renombrados de la Admin API.
26
+
27
+ ### Bases de confianza del repo
28
+
29
+ Qué hace Parche con tu código, cómo reportar una vulnerabilidad y cómo contribuir.
30
+
31
+ - **`SECURITY.md`:** cómo reportar una vulnerabilidad (en privado, primera respuesta en 14 días como máximo) y qué hace Parche en tu máquina o en tu runner: a qué hosts se conecta y con qué comando, que no tiene telemetría, qué ejecuta (tus tests, solo con `--verify`) y qué archivos escribe.
32
+ - **README:** un bloque en inglés al principio, la sección "Seguridad y datos" y qué puede cambiar dentro de `v0`.
33
+ - **`CONTRIBUTING.md`** (con el proceso para sacar una versión, que sale del README), **código de conducta**, plantillas de issue (falso positivo o aviso que falta, pedido de pack, error) y de PR.
34
+ - **`parche --version`.**
35
+ - **"¿Por qué recibí un PR de Parche?"** ([`docs/por-que-recibi-este-pr.md`](docs/por-que-recibi-este-pr.md)): para quien recibe un PR o un issue de Parche. Explica por qué su repo, cómo se revisó el PR, qué se verificó y qué no, y cómo pedir que no le manden más. El pie de cada PR y de cada issue la enlaza.
36
+ - **La plantilla del workflow pide solo lectura por defecto** y da `contents: write` y `pull-requests: write` solo al job de Parche. Explica cómo fijar la Action a un commit en vez de `v0`.
37
+ - **Las Actions de la CI, del release y de la plantilla quedan fijadas por SHA** (`actions/checkout` v7.0.1, `actions/setup-node` v7.1.0), con `dependabot.yml` para mantenerlas al día.
38
+ - `package.json`: la descripción dice "cualquier API", la página es getparche.com y suma keywords de IA.
39
+
40
+ ## 0.6.6 — 2026-10-20
41
+
42
+ 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.
43
+
44
+ ### Nuevo
45
+ - **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`).
46
+ - **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.
47
+ - **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.
48
+ - **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.
49
+
50
+ ### Todavía no cubre
51
+ - 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.
52
+ - 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-…`).
53
+ - Los SDKs de Go, Java, .NET, Ruby y PHP, y los cuerpos de pedido escritos como texto JSON dentro de un string.
4
54
 
5
55
  ## 0.6.5 — 2026-10-19
6
56
 
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Parche — Dependabot para APIs
2
2
 
3
3
  [![CI](https://github.com/Pepo08/parche/actions/workflows/ci.yml/badge.svg)](https://github.com/Pepo08/parche/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/parche-cli)](https://www.npmjs.com/package/parche-cli)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
+
7
+ > **In English.** Parche reads your code and inventories every third-party API it uses: SDKs, direct HTTP calls and AI model IDs. When a provider ships a dated change that affects you, it tells you the file and the line, or opens a small PR with a deterministic fix. Every changed file is parsed before the PR, and with `--verify` your own tests run too. What can't be inferred is never made up: the PR asks for it. Today's packs cover Argentina's ARCA e-invoicing change (due December 1, 2026), Mercado Pago Payments → Orders, and OpenAI and Anthropic model and parameter deprecations.
8
+ >
9
+ > Try it with `npx parche-cli scan .` (Node 22+): it runs locally, with no account and no telemetry ([security and data](SECURITY.md)). GitHub Action: `uses: Pepo08/parche@v0`. The rest of this README is in Spanish for now · [getparche.com](https://getparche.com) · hola@getparche.com
4
10
 
5
11
  Parche lee tu código y arma el inventario de **todas** las APIs de terceros que usás: SDKs (Stripe, Twilio, OpenAI, AWS, Mercado Pago, ARCA…), llamadas HTTP directas y modelos de IA. Lo cruza contra una base de **cambios de API verificados** y, cuando un proveedor cambia algo que te afecta, te avisa o abre un PR con el arreglo, explica qué cambió y por qué, y marca lo que necesita una persona. Ningún PR sale sin pasar la verificación. Las APIs de pagos y facturación de Latinoamérica son el fuerte, pero el inventario cubre cualquier API.
6
12
 
@@ -20,7 +26,7 @@ APIs del repo (6):
20
26
  👀 revisar src/facturacion.js:56 Agregado CondicionIVAReceptorId: 5 por inferencia (DNI + comprobante B/C)…
21
27
  ✋ falta un dato src/facturacion.js:62 Falta CondicionIVAReceptorId y no se puede inferir… (comprobante clase A).
22
28
 
23
- ■ Modelos y endpoints de IA retirados o por retirarse
29
+ ■ Modelos, endpoints y parámetros de IA retirados o por retirarse
24
30
  🔎 detectado src/ia.js:9 El modelo `claude-3-5-haiku-20241022` fue retirado el 2026-02-19: las llamadas ya fallan…
25
31
  …
26
32
  ```
@@ -87,6 +93,14 @@ Para que corra todas las semanas, copiá [`templates/github-workflow.yml`](templ
87
93
 
88
94
  El workflow necesita `permissions: contents: write` y `pull-requests: write`.
89
95
 
96
+ Con el `GITHUB_TOKEN` por defecto, GitHub deja los workflows del PR que abre Parche esperando aprobación: alguien con permiso de escritura tiene que tocar **Approve workflows to run** (el cuerpo del PR lo avisa). Para que tu CI corra sola sobre esos PRs, pasale a la Action un token de una GitHub App o un token personal con permiso de escritura en `contents` y `pull-requests`:
97
+
98
+ ```yaml
99
+ - uses: Pepo08/parche@v0
100
+ with:
101
+ token: ${{ secrets.PARCHE_TOKEN }}
102
+ ```
103
+
90
104
  ### Desarrollo
91
105
 
92
106
  ```bash
@@ -98,22 +112,7 @@ node bin/parche.js scan examples/saas-demo # SaaS con Stripe, Twilio, OpenAI
98
112
  node bin/parche.js scan examples/legacy-ia --upgrades # OpenAI v3: qué se rompe al actualizar (usa la red)
99
113
  ```
100
114
 
101
- #### Sacar una versión
102
-
103
- Las versiones se publican solas desde GitHub Actions ([`release.yml`](.github/workflows/release.yml)) con [trusted publishing](https://docs.npmjs.com/trusted-publishers): npm confía en este repo y en ese workflow, sin tokens ni 2FA a mano. Desde `main` actualizado:
104
-
105
- ```bash
106
- npm version 0.1.2 --no-git-tag-version # cambia package.json y package-lock.json
107
- git commit -am "Parche 0.1.2"
108
- git tag -a v0.1.2 -m "Parche 0.1.2: qué cambió" # el mensaje del tag es el texto del GitHub Release
109
- git push origin main v0.1.2
110
- ```
111
-
112
- Con el tag, el workflow corre los tests, controla que la versión de `package.json` coincida con el tag y que el commit esté en `main`, publica `parche-cli` en npm (con provenance), mueve `v0` a esta versión (es la que usan los workflows con `Pepo08/parche@v0`) y crea el GitHub Release. Para notas largas, `git tag -a v0.1.2 -F notas.md`.
113
-
114
- - Solo dispara con versiones estables (`vX.Y.Z`); un tag como `v0.2.0-beta.1` no hace nada.
115
- - Si falla un paso después de publicar, "Re-run failed jobs" retoma: lo que ya está hecho se saltea.
116
- - Requiere, una sola vez, el Trusted Publisher en npmjs.com: `parche-cli` → Settings → Trusted publishing → GitHub Actions, con `Pepo08` / `parche` / `release.yml` y permiso para `npm publish`.
115
+ Cómo contribuir, las reglas para el código y cómo se saca una versión: [`CONTRIBUTING.md`](CONTRIBUTING.md).
117
116
 
118
117
  ## Estados de cada hallazgo
119
118
 
@@ -131,7 +130,7 @@ Con el tag, el workflow corre los tests, controla que la versión de `package.js
131
130
  |---|---|---|---|---|
132
131
  | `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
132
  | `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) |
133
+ | `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, `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), y los mismos tres parámetros con cualquier modelo en la API version `2026-10-21`, que usan el SDK de TypeScript 1.0.0 y el de Python 2.0.0 (del 21/10/2026): dice si el pedido ya falla, si falla en una instalación nueva o si va a fallar cuando suban el SDK |
135
134
 
136
135
  ## Verificación
137
136
 
@@ -141,6 +140,21 @@ Antes de commitear, Parche parsea **siempre** cada archivo modificado (JS/TS o P
141
140
 
142
141
  **Copias de respaldo:** Parche no lee ni edita carpetas `BAK/`, `backup/`, `old/`, `_old/` y `deprecated/`, ni archivos como `factura copy.php`, `factura - Copy.js`, `factura.bak.php` o `factura-old.php`. El scan dice cuáles saltó, por si alguno es código que corre.
143
142
 
143
+ ## Seguridad y datos
144
+
145
+ Parche corre en tu máquina o en tu runner. Tu código no sale de ahí, salvo el PR que abre en tu propio repo.
146
+
147
+ | | |
148
+ |---|---|
149
+ | **Red** | `scan`, `fix` y `packs` no usan la red. `pr` llama a `api.github.com` para abrir el PR en tu repo y pushea su rama `parche/<pack>` (con `--force`, así solo reemplaza su propio PR anterior). `diff` y `scan --upgrades` bajan metadatos y paquetes de `registry.npmjs.org`, que ve qué paquetes y versiones consultás. Nada más. |
150
+ | **Telemetría** | Ninguna. `parche feedback` escribe en `.parche/ledger.jsonl` de tu repo y queda ahí. |
151
+ | **Qué ejecuta** | `git` y, solo con `--verify`, los tests de tu repo (`npm test`, `composer test` o `vendor/bin/phpunit`). |
152
+ | **Qué escribe** | Tu working copy (solo `fix` y `pr`), `.parche/` (se agrega a `.git/info/exclude`) y, para `diff` y `--upgrades`, un caché en `~/.parche/cache`. |
153
+
154
+ Para reportar una vulnerabilidad, y el detalle de todo esto: [`SECURITY.md`](SECURITY.md). Si te llegó un PR de Parche: [por qué lo recibiste](docs/por-que-recibi-este-pr.md).
155
+
156
+ **Qué puede cambiar dentro de `v0`:** mientras Parche esté en 0.x, `Pepo08/parche@v0` y `npx parche-cli` reciben cada versión nueva. Una versión 0.x puede sumar packs, avisos y opciones. No cambia sin aviso en el CHANGELOG el formato de `.parche/inventory.json` (tiene `schemaVersion`), los nombres de los comandos ni el de los inputs de la Action. Si tu organización exige fijar las Actions, usá el commit de un release: `Pepo08/parche@<sha> # v0.6.7`.
157
+
144
158
  ## ARCA en PHP
145
159
 
146
160
  Para `afipsdk/afip.php` y sus forks (`fertandil87/afip.php`, `ivanalemunioz/afip.php`, `eesnaola/afip.php`), Parche busca `$afip->ElectronicBilling->CreateVoucher($data)` y `CreateNextVoucher($data)` (también en minúsculas: PHP no distingue) y agrega una línea por factura:
@@ -188,6 +202,8 @@ Cuando `DocTipo` no es un número fijo (`DocTipo: docTipo`), Parche no puede sab
188
202
 
189
203
  - JS/TS (ESM y CommonJS) y, para ARCA, PHP. Siguen Python y Java.
190
204
  - 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.
205
+ - 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.
206
+ - IA, API version `2026-10-21` de Anthropic: la versión del SDK sale de `package-lock.json`, `node_modules` o el rango de `package.json`, y en Python solo de `requirements.txt` (todavía no de `pyproject.toml`, `poetry.lock` ni `uv.lock`). Un rango sin número (`latest`, `*`) cuenta como versión desconocida. Por HTTP directo avisa solo si el archivo manda `anthropic-version: 2026-10-21`. Los SDKs de Go, Java, .NET, Ruby y PHP no se leen.
191
207
  - 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
208
  - 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
209
  - 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/bin/parche.js CHANGED
@@ -27,6 +27,7 @@ Uso:
27
27
  pushea a ese remote (tu fork) y abre el PR contra origin.
28
28
  Con --author, firma con esa identidad y no con la de git
29
29
  parche feedback <dir> --pack <id> --outcome merged|edited|rejected [--note "..."]
30
+ parche --version La versión instalada (para reportar un error)
30
31
  `;
31
32
 
32
33
  function parseArgs(argv) {
@@ -140,6 +141,11 @@ function printFindings(result) {
140
141
  async function main() {
141
142
  const args = parseArgs(process.argv.slice(2));
142
143
  const [cmd, dirArg] = args._;
144
+ if (args.version || cmd === '-v') {
145
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
146
+ console.log(pkg.version);
147
+ return 0;
148
+ }
143
149
  if (!cmd || args.help) { console.log(HELP); return 0; }
144
150
 
145
151
  if (cmd === 'packs') {
@@ -243,7 +249,9 @@ async function main() {
243
249
  }
244
250
  // Si solo cambiarían tests o mocks no hay PR: el texto queda como issue, sin "Modifica 1 archivo" (frente B, punto 20).
245
251
  const reportOnly = !changedFiles.length || changedFiles.every(isTestFile);
246
- fs.writeFileSync(bodyPath, prBody({ pack, findings: result.findings, changed: reportOnly ? [] : result.changed, verification: reportOnly ? null : verification }));
252
+ // La Action avisa si usa el GITHUB_TOKEN por defecto (PARCHE_DEFAULT_TOKEN): los workflows del PR esperan aprobación.
253
+ const ciApproval = process.env.GITHUB_ACTIONS === 'true' && process.env.PARCHE_DEFAULT_TOKEN === 'true';
254
+ fs.writeFileSync(bodyPath, prBody({ pack, findings: result.findings, changed: reportOnly ? [] : result.changed, verification: reportOnly ? null : verification, ciApproval }));
247
255
  if (reportOnly) {
248
256
  const why = changedFiles.length ? 'solo cambiaría tests o mocks: no se abre un PR' : 'sin cambios de código';
249
257
  git.discardChanges(root);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.6.5",
4
- "description": "Dependabot para APIs: detecta cambios de APIs de terceros en tu código y abre el PR con el arreglo.",
3
+ "version": "0.6.7",
4
+ "description": "Dependabot para APIs: detecta cambios de cualquier API de terceros en tu código (ARCA, Mercado Pago, OpenAI, Anthropic) y abre el PR con el arreglo.",
5
5
  "keywords": [
6
6
  "api",
7
7
  "migration",
@@ -11,9 +11,15 @@
11
11
  "afip",
12
12
  "wsfe",
13
13
  "mercadopago",
14
- "github-action"
14
+ "github-action",
15
+ "openai",
16
+ "anthropic",
17
+ "claude",
18
+ "api-migration",
19
+ "deprecation",
20
+ "breaking-changes"
15
21
  ],
16
- "homepage": "https://github.com/Pepo08/parche#readme",
22
+ "homepage": "https://getparche.com",
17
23
  "bugs": "https://github.com/Pepo08/parche/issues",
18
24
  "repository": {
19
25
  "type": "git",
@@ -1,16 +1,27 @@
1
1
  {
2
2
  "id": "ai.model-retirements",
3
- "version": "2026.10.19",
3
+ "version": "2026.10.21",
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)", "url": "https://platform.claude.com/docs/en/models/opus-5-5/migration-guide" },
18
+ { "title": "Anthropic: Migrating to API version 2026-10-21 (temperature, top_p y top_k dan 400 con cualquier modelo; primera versión de cada SDK que la usa)", "url": "https://platform.claude.com/docs/en/api/migration-2026-10-21" },
19
+ { "title": "Anthropic: Versions (anthropic-version; historial de la API version 2026-10-21)", "url": "https://platform.claude.com/docs/en/api/versioning" },
20
+ { "title": "anthropic-sdk-python: MIGRATION.md de la 1.0.0 (temperature, top_p y top_k salen de los métodos: TypeError)", "url": "https://github.com/anthropics/anthropic-sdk-python/blob/v1.0.0/MIGRATION.md" },
21
+ { "title": "anthropic-sdk-python: release v2.0.0 del 21/10/2026 (API version 2026-10-21)", "url": "https://github.com/anthropics/anthropic-sdk-python/releases/tag/v2.0.0" },
22
+ { "title": "anthropic-sdk-typescript: release sdk-v1.0.0 del 21/10/2026 (API version 2026-10-21)", "url": "https://github.com/anthropics/anthropic-sdk-typescript/releases/tag/sdk-v1.0.0" },
23
+ { "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" },
24
+ { "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
25
  { "title": "OpenAI: Deprecations", "url": "https://developers.openai.com/api/docs/deprecations" },
15
26
  { "title": "OpenAI: Supervised fine-tuning (formato de los IDs ft:)", "url": "https://developers.openai.com/api/docs/guides/supervised-fine-tuning" },
16
27
  { "title": "OpenAI: Models (snapshots de cada familia y de cada modelo base)", "url": "https://developers.openai.com/api/docs/models" },
@@ -163,6 +174,42 @@
163
174
  "claude-haiku-4-5-20251001": "2026-10-15",
164
175
  "claude-opus-4-5-20251101": "2026-11-24"
165
176
  },
177
+ "sampling_params": {
178
+ "last_verified": "2026-10-20",
179
+ "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",
180
+ "claude-fable-5", "claude-fable-5-1", "claude-mythos-5", "claude-mythos-5-1", "claude-mythos-preview"],
181
+ "scope_es": "en Claude 4.7 y posteriores (y en Claude Mythos Preview)",
182
+ "accepted": {
183
+ "temperature": { "min": 1, "max": 1, "es": "solo acepta 1, su valor por defecto" },
184
+ "top_p": { "min": 0.99, "max": 1, "es": "solo acepta valores desde 0.99" },
185
+ "top_k": { "es": "no acepta ningún valor" }
186
+ },
187
+ "overrides": {
188
+ "claude-haiku-5-5": {
189
+ "scope_es": "en Claude Haiku 5.5",
190
+ "accepted": { "top_p": { "min": 0.99, "max": 0.99, "es": "solo acepta 0.99, su valor por defecto (ni siquiera 1)" } },
191
+ "exclusive": ["temperature", "top_p"],
192
+ "exclusive_es": "Claude Haiku 5.5 rechaza los pedidos que mandan los dos, aunque tengan sus valores por defecto"
193
+ }
194
+ },
195
+ "recommendation_es": "sacar {params} del pedido y guiar el comportamiento del modelo con el prompt",
196
+ "temperature_zero_es": "Si estaba en 0 para tener respuestas determinísticas: según Anthropic, eso nunca garantizó respuestas idénticas.",
197
+ "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`.",
198
+ "api_version": {
199
+ "version": "2026-10-21",
200
+ "last_verified": "2026-10-21",
201
+ "released_es": "21/10/2026",
202
+ "sdks": {
203
+ "npm": { "package": "@anthropic-ai/sdk", "first": "1.0.0" },
204
+ "pypi": { "package": "anthropic", "first": "2.0.0", "typeerror_from": "1.0.0" }
205
+ },
206
+ "removed_es": "esa versión de la API eliminó `temperature`, `top_p` y `top_k`: con cualquier modelo devuelven un error 400, aunque tengan su valor por defecto",
207
+ "recommendation_es": "sacá {param} del pedido y guiá el comportamiento del modelo con el prompt",
208
+ "typescript_es": "En TypeScript, además, el SDK 1.0 ya no tiene el parámetro en sus tipos.",
209
+ "older_models": ["claude-sonnet-4-6", "claude-sonnet-4-5", "claude-sonnet-4-2", "claude-opus-4-6", "claude-opus-4-5", "claude-opus-4-1", "claude-opus-4-2", "claude-haiku-4-5", "claude-3"],
210
+ "older_model_es": "Con {model}, que todavía acepta otros valores, sacarlo cambia cómo responde y ningún otro ajuste lo reemplaza: si dependés de ese valor, ese pedido tiene que quedarse en la API version `2023-06-01`."
211
+ }
212
+ },
166
213
  "retired_endpoints": [
167
214
  { "pattern": ".beta.assistants", "retired": "2026-08-26", "api_es": "Assistants API", "replacement_es": "Responses API + Conversations API", "provider": "openai" },
168
215
  { "pattern": ".beta.threads", "retired": "2026-08-26", "api_es": "Assistants API", "replacement_es": "Responses API + Conversations API", "provider": "openai" },
@@ -182,6 +229,6 @@
182
229
  "review_checklist_es": [
183
230
  "Elegir el modelo de reemplazo según la página de deprecaciones del proveedor.",
184
231
  "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."
232
+ "Revisar parámetros que cambian entre generaciones de modelos (desde Claude 4.7, `temperature`, `top_p` y `top_k`)."
186
233
  ]
187
234
  }
package/src/context.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // Contexto del repo para los packs: lo que un archivo solo no puede saber.
2
2
  // Lo arma el motor una vez, antes de correr los packs, y cada transform lo recibe como `context`.
3
3
  //
4
- // versions: versión instalada de cada dependencia (de los lockfiles, de node_modules o del rango declarado)
4
+ // versions: versión instalada de cada dependencia (de los lockfiles, de node_modules o del rango declarado;
5
+ // en Python, solo el rango de requirements.txt). Si sale del rango, `range` trae el texto declarado.
5
6
  // fieldFiles: archivos que ya usan CondicionIVAReceptorId (el repo ya empezó a migrar)
6
7
  // staleWsdl: WSDL de WSFEv1 del repo que no tienen CondicionIVAReceptorId
7
8
  // arcaLibs: librerías de ARCA del repo con lo que se sabe de si el campo llega a viajar
@@ -20,8 +21,10 @@ const clean = (v) => String(v || '').replace(/^v/, '');
20
21
 
21
22
  // Versiones instaladas por ecosistema: lockfiles (en cualquier carpeta) → node_modules de la raíz → rango declarado.
22
23
  export function readVersions(root, manifests) {
23
- const out = { npm: {}, composer: {} };
24
- const set = (eco, name, version, source) => { if (name && version && !out[eco][name]) out[eco][name] = { version: clean(version), source }; };
24
+ const out = { npm: {}, composer: {}, pypi: {} };
25
+ const set = (eco, name, version, source, range) => {
26
+ if (name && version && !out[eco][name]) out[eco][name] = { version: clean(version), source, ...(range ? { range } : {}) };
27
+ };
25
28
  for (const rel of findFiles(root, (n) => n === 'package-lock.json' || n === 'composer.lock')) {
26
29
  const lock = readJson(path.join(root, rel));
27
30
  if (!lock) continue;
@@ -39,15 +42,19 @@ export function readVersions(root, manifests) {
39
42
  const pkg = readJson(path.join(root, 'node_modules', name, 'package.json'));
40
43
  if (pkg?.version) set('npm', name, pkg.version, 'node_modules');
41
44
  }
42
- for (const eco of ['npm', 'composer']) {
45
+ for (const eco of ['npm', 'composer', 'pypi']) {
43
46
  for (const [name, range] of Object.entries(manifests?.[eco] || {})) {
44
47
  const m = String(range).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
45
- if (m) set(eco, name, `${m[1]}.${m[2] || 0}.${m[3] || 0}`, 'declarada');
48
+ if (m) set(eco, name, `${m[1]}.${m[2] || 0}.${m[3] || 0}`, 'declarada', String(range));
46
49
  }
47
50
  }
48
51
  return out;
49
52
  }
50
53
 
54
+ // ¿Un rango declarado deja instalar versiones mayores nuevas? `>=0.40`, `>0.3`, `*`, `x` o `latest` sin tope (`<`):
55
+ // una instalación nueva trae la última versión. `^0.137.0`, `~1.2`, `==1.4.0` o `~=1.2` no.
56
+ export const openRange = (range) => /^\s*(?:>=?|\*|x|latest)/i.test(range || '') && !/</.test(range || '');
57
+
51
58
  // Qué se sabe, por librería, de si CondicionIVAReceptorId llega a ARCA (verificado en su código el 16/10/2026):
52
59
  // afip.js / afip.php ≥ 1.0: mandan JSON a la API de Afip SDK → sí.
53
60
  // 0.x: arman el SOAP con un WSDL de 2021 incluido en la librería, sin el campo → no (PHP) o sin confirmar (JS).
@@ -90,4 +97,4 @@ export function buildContext(root, files, manifests) {
90
97
  return { versions, fieldFiles, staleWsdl, arcaLibs: arcaLibraries(versions) };
91
98
  }
92
99
 
93
- export const emptyContext = () => ({ versions: { npm: {}, composer: {} }, fieldFiles: [], staleWsdl: [], arcaLibs: [] });
100
+ export const emptyContext = () => ({ versions: { npm: {}, composer: {}, pypi: {} }, fieldFiles: [], staleWsdl: [], arcaLibs: [] });
@@ -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;
@@ -36,7 +38,7 @@ const endpointHit = (ep, l) => {
36
38
  const milestonesText = (ep, today) => (ep.milestones || [])
37
39
  .map((m) => (m.date <= today ? ` Desde el ${m.date} ${m.past_es}.` : ` El ${m.date} ${m.future_es}.`)).join('');
38
40
 
39
- export default function detect({ source, file, pack, today = new Date().toISOString().slice(0, 10) }) {
41
+ export default function detect({ source, file, pack, today = new Date().toISOString().slice(0, 10), context }) {
40
42
  const findings = [];
41
43
  const usages = [];
42
44
  const { retired_models: retired, retirement_floors: floors, retired_endpoints: endpoints } = pack.detect;
@@ -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` (Claude 4.7 y posteriores, API version 2026-10-21): 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, context });
100
+ findings.push(...sampling.findings);
101
+ usages.push(...sampling.usages);
95
102
  return { output: null, findings, usages };
96
103
  }
@@ -0,0 +1,408 @@
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
+ //
20
+ // API version `2026-10-21` (D65): elimina los tres parámetros para cualquier modelo (400 aunque tengan su valor por
21
+ // defecto). La manda el archivo (`anthropic-version: 2026-10-21`) o el SDK desde su versión mayor nueva (TypeScript 1.0.0
22
+ // y Python 2.0.0, del 21/10/2026); los anteriores mandan `2023-06-01`. Corre sobre los mismos pedidos, sin importar el
23
+ // modelo, si el archivo es de Anthropic (importa el SDK, nombra un `claude-…` o va por HTTP a api.anthropic.com), y dice
24
+ // si ya falla (header, SDK instalado o, en Python, `TypeError` desde la 1.0), si falla en una instalación nueva (rango
25
+ // sin tope) o si va a fallar cuando suban el SDK. Lo que ya avisó la regla de Claude 4.7 no se repite. Por HTTP sin el
26
+ // header nuevo no se avisa: la versión la elige el archivo y no cambia sola.
27
+ import { openRange } from '../context.js';
28
+
29
+ const PACK = 'ai.model-retirements';
30
+ const JS = /\.[cm]?[jt]sx?$/i;
31
+ const PY = /\.py$/i;
32
+ const PARAMS = ['temperature', 'top_p', 'top_k'];
33
+ const MESSAGES_CALL = /\.\s*messages\s*\.\s*(?:with_(?:raw|streaming)_response\s*\.\s*)?(?:create|stream)\s*$/;
34
+ const OPENAI_CALL = /\.\s*(?:chat\s*\.\s*)?completions\s*\.\s*create\s*$|\.\s*responses\s*\.\s*(?:create|stream|parse)\s*$/;
35
+ const TYPED_PARAMS = /MessageCreateParams\w*(?:<[^>]*>)?\s*=\s*$/;
36
+ const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
37
+ const IDENT = /^[A-Za-z_$][\w$]*$/;
38
+ const NAMES = /[A-Za-z_$][\w$]*/g;
39
+ // Antes de una `/` que abre una regex de JS (y no es una división) hay un operador, un separador o una palabra clave.
40
+ const REGEX_BEFORE = /(?:^|[(,=:[!&|?{};+\-*%<>~^]|\b(?:return|typeof|case|in|of|yield|await|void|delete|throw|else|do))\s*$/;
41
+ const CONTROL = /\b(?:if|for|while|switch|catch|with)\s*$/;
42
+ const ANTHROPIC = /@anthropic-ai\/|["'`]claude-|^\s*(?:from\s+anthropic(?:\.\w+)*\s+import|import\s+anthropic)\b/m;
43
+ const TS = /\.[cm]?tsx?$/i;
44
+ const EMPTY = /^(?:undefined|null|None)$/;
45
+
46
+ // a ≥ b, comparando "mayor.menor.parche".
47
+ const atLeast = (a, b) => {
48
+ const x = String(a).split('.').map(Number);
49
+ const y = String(b).split('.').map(Number);
50
+ for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) > (y[i] || 0);
51
+ return true;
52
+ };
53
+
54
+ // Copia del código con los comentarios y el interior de los strings en blanco (mismas posiciones y saltos de línea),
55
+ // para buscar llaves, comas y claves sin tropezar con texto. Los strings quedan aparte, con su valor.
56
+ function tokenize(source, python) {
57
+ const code = source.split('');
58
+ const strings = [];
59
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (code[k] !== '\n' && code[k] !== '\r') code[k] = ' '; };
60
+ const n = source.length;
61
+ let i = 0;
62
+ while (i < n) {
63
+ const c = source[i];
64
+ if (python ? c === '#' : source.startsWith('//', i)) {
65
+ const e = source.indexOf('\n', i);
66
+ const end = e < 0 ? n : e;
67
+ blank(i, end); i = end; continue;
68
+ }
69
+ if (!python && source.startsWith('/*', i)) {
70
+ const e = source.indexOf('*/', i + 2);
71
+ const end = e < 0 ? n : e + 2;
72
+ blank(i, end); i = end; continue;
73
+ }
74
+ if (c === '"' || c === "'" || (c === '`' && !python)) {
75
+ const q = python && source.startsWith(c.repeat(3), i) ? c.repeat(3) : c;
76
+ let j = i + q.length;
77
+ while (j < n && !source.startsWith(q, j)) {
78
+ if (source[j] === '\\') j++;
79
+ else if (source[j] === '\n' && q.length === 1 && c !== '`') break; // string sin cerrar: termina en la línea
80
+ j++;
81
+ }
82
+ const end = Math.min(n, j + q.length);
83
+ strings.push({ start: i, end, value: source.slice(i + q.length, j) });
84
+ blank(i + q.length, j);
85
+ i = end; continue;
86
+ }
87
+ if (!python && c === '/' && REGEX_BEFORE.test(code.slice(Math.max(0, i - 12), i).join(''))) {
88
+ let j = i + 1;
89
+ let inClass = false;
90
+ while (j < n && source[j] !== '\n' && (inClass || source[j] !== '/')) {
91
+ if (source[j] === '\\') j++;
92
+ else if (source[j] === '[') inClass = true;
93
+ else if (source[j] === ']') inClass = false;
94
+ j++;
95
+ }
96
+ blank(i + 1, j);
97
+ i = j + 1; continue;
98
+ }
99
+ i++;
100
+ }
101
+ return { code: code.join(''), strings };
102
+ }
103
+
104
+ // Árbol de paréntesis, corchetes y llaves, y la profundidad de cada posición. Un cierre que no corresponde al último
105
+ // abierto cierra hasta su par (código a medio escribir o una regex que no se reconoció); si no tiene par, se ignora.
106
+ function brackets(code) {
107
+ const PAIR = { ')': '(', ']': '[', '}': '{' };
108
+ const nodes = [];
109
+ const stack = [];
110
+ const depth = new Uint16Array(code.length + 1);
111
+ for (let i = 0; i < code.length; i++) {
112
+ const c = code[i];
113
+ depth[i] = stack.length;
114
+ if (c === '(' || c === '[' || c === '{') {
115
+ const node = { open: c, start: i, end: -1, parent: stack.at(-1) || null };
116
+ stack.push(node);
117
+ nodes.push(node);
118
+ } else if (PAIR[c]) {
119
+ const k = stack.map((x) => x.open).lastIndexOf(PAIR[c]);
120
+ if (k < 0) continue;
121
+ for (const x of stack.splice(k)) x.end = i;
122
+ }
123
+ }
124
+ return { nodes: nodes.filter((x) => x.end >= 0), depth };
125
+ }
126
+
127
+ // Primer separador de primer nivel en code[a, b): `:` en objetos y diccionarios, `=` en argumentos con nombre de Python
128
+ // (no `==`, `<=`, `>=`, `!=`).
129
+ function separator(code, a, b, sep) {
130
+ let level = 0;
131
+ for (let i = a; i < b; i++) {
132
+ const c = code[i];
133
+ if ('([{'.includes(c)) level++;
134
+ else if (')]}'.includes(c)) level--;
135
+ else if (level === 0 && c === sep) {
136
+ if (sep === '=' && (code[i + 1] === '=' || '=!<>'.includes(code[i - 1]))) continue;
137
+ return i;
138
+ }
139
+ }
140
+ return -1;
141
+ }
142
+
143
+ export default function detectSamplingParams({ source, file, pack, context }) {
144
+ const rules = pack.detect.sampling_params;
145
+ const python = PY.test(file);
146
+ if (!rules || (!python && !JS.test(file))) return { findings: [], usages: [] };
147
+ // La mayoría de los archivos no tiene el parámetro, ni un modelo afectado ni nada de Anthropic: no hace falta
148
+ // tokenizarlos.
149
+ const api = rules.api_version;
150
+ const anthropic = Boolean(api) && (ANTHROPIC.test(source) || (/api\.anthropic\.com/.test(source) && /v1\/messages/.test(source)));
151
+ if (!PARAMS.some((p) => source.includes(p)) || (!anthropic && !rules.models.some((m) => source.includes(m)))) return { findings: [], usages: [] };
152
+
153
+ const { code, strings } = tokenize(source, python);
154
+ const { nodes, depth } = brackets(code);
155
+ const stringAt = new Map(strings.map((s) => [s.start, s]));
156
+ const lineStarts = [0];
157
+ for (let i = 0; i < source.length; i++) if (source[i] === '\n') lineStarts.push(i + 1);
158
+ const lineOf = (pos) => {
159
+ let lo = 0;
160
+ let hi = lineStarts.length - 1;
161
+ while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (lineStarts[mid] <= pos) lo = mid; else hi = mid - 1; }
162
+ return lo + 1;
163
+ };
164
+ // Texto de code[a, b) sin los espacios (ni los comentarios) de los bordes, tomado del original. Sin `as const` ni
165
+ // `satisfies T` de TypeScript al final.
166
+ const trimmed = (a, b) => {
167
+ while (a < b && /\s/.test(code[a])) a++;
168
+ while (b > a && /\s/.test(code[b - 1])) b--;
169
+ const cast = python ? null : code.slice(a, b).match(/\s+(?:as|satisfies)\s+[\w$.<>\[\]]+$/);
170
+ if (cast) b -= cast[0].length;
171
+ return { at: a, end: b, text: source.slice(a, b) };
172
+ };
173
+ const literal = (v) => { const s = stringAt.get(v.at); return s && s.end === v.end ? s.value : null; };
174
+ const before = (node) => code.slice(Math.max(0, node.start - 160), node.start);
175
+ const nodeAt = (pos) => nodes.find((x) => x.start === pos) || null;
176
+
177
+ // Asignaciones del archivo. Una variable cuenta como constante si se asigna una sola vez, en una declaración
178
+ // (`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
179
+ // le da un valor a un nombre (parámetros de funciones, desestructuración, imports, `for`, `+=`) cuenta como otra
180
+ // asignación: así un parámetro que se llama igual que una constante no hace leer un valor que no es.
181
+ let assigned = null;
182
+ const assignments = () => {
183
+ if (assigned) return assigned;
184
+ assigned = new Map();
185
+ const add = (name, at, valueAt) => {
186
+ const list = assigned.get(name) || [];
187
+ if (!list.some((x) => x.at === at)) list.push({ at, valueAt });
188
+ assigned.set(name, list);
189
+ };
190
+ const bind = (a, b) => { for (const m of code.slice(a, b).matchAll(NAMES)) add(m[0], a + m.index, null); };
191
+ if (python) {
192
+ for (const m of code.matchAll(/^[ \t]*([A-Za-z_]\w*)[ \t]*(?::[^=\n]*)?=(?!=)/gm)) {
193
+ const at = m.index + m[0].indexOf(m[1]);
194
+ if (depth[at] === 0) add(m[1], at, m.index + m[0].length);
195
+ }
196
+ for (const m of code.matchAll(/^[ \t]*([A-Za-z_]\w*)[ \t]*(?:[-+*/%@&|^]|\/\/|\*\*|>>|<<)=/gm)) add(m[1], m.index + m[0].indexOf(m[1]), null);
197
+ 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]));
198
+ 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);
199
+ 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);
200
+ for (const x of nodes) if (x.open === '(' && /\bdef\s+\w+\s*$/.test(before(x))) bind(x.start + 1, x.end);
201
+ } else {
202
+ for (const m of code.matchAll(/(?<![\w$.])(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]*)?=(?![=>])/g)) {
203
+ add(m[1], m.index + m[0].indexOf(m[1], m[0].search(/\s/)), m.index + m[0].length);
204
+ }
205
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:[-+*/%&|^]|\*\*|\?\?|\|\||&&|>>>?|<<)?=(?![=>])/g)) add(m[1], m.index, null);
206
+ for (const m of code.matchAll(/(?<![\w$.])(?:const|let|var)\s*([{[])/g)) {
207
+ const x = nodeAt(m.index + m[0].length - 1);
208
+ if (x) bind(x.start, x.end);
209
+ }
210
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*=>/g)) add(m[1], m.index, null);
211
+ for (const m of code.matchAll(/^[ \t]*import\s+([^;\n]*?)\s+from\b/gm)) bind(m.index, m.index + m[0].length);
212
+ for (const x of nodes) {
213
+ if (x.open === '(' && !CONTROL.test(before(x)) && /^\s*(?::[^={;]*)?(?:=>|\{)/.test(code.slice(x.end + 1, x.end + 120))) bind(x.start + 1, x.end);
214
+ }
215
+ }
216
+ const lineEnd = (i) => { const e = code.indexOf('\n', i); return e < 0 ? code.length : e; };
217
+ for (const list of assigned.values()) {
218
+ for (const x of list) {
219
+ if (x.valueAt === null) continue;
220
+ let { at, end } = trimmed(x.valueAt, lineEnd(x.valueAt));
221
+ while (end > at && (code[end - 1] === ';' || code[end - 1] === ',' || /\s/.test(code[end - 1]))) end--;
222
+ x.value = trimmed(at, end);
223
+ }
224
+ }
225
+ return assigned;
226
+ };
227
+ const constant = (name) => {
228
+ const list = assignments().get(name) || [];
229
+ return list.length === 1 && list[0].value ? list[0].value : null;
230
+ };
231
+
232
+ // El modelo de un valor: literal, constante o el valor por defecto de una variable. `from` dice de dónde sale.
233
+ const modelOf = (v, nested = false) => {
234
+ const lit = literal(v);
235
+ if (lit !== null) return { id: lit, from: null };
236
+ const t = v.text;
237
+ if (IDENT.test(t)) { const c = !nested && constant(t); return c ? modelOf(c, true) : null; }
238
+ const js = !python && t.match(/^(process\.env(?:\.\w+|\[\s*(['"])\w+\2\s*\])|[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)*)\s*(?:\|\||\?\?)\s*(['"`])([^'"`\\$]*)\3$/);
239
+ if (js) return { id: js[4], from: `\`${js[1]}\`` };
240
+ const env = python && t.match(/^(?:os\.)?(?:getenv|environ\.get)\(\s*(['"])(\w+)\1\s*,\s*(['"])([^'"\\]*)\3\s*\)$/);
241
+ if (env) return { id: env[4], from: `la variable de entorno \`${env[2]}\`` };
242
+ const or = python && t.match(/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s+or\s+(['"])([^'"\\]*)\2$/);
243
+ if (or) return { id: or[3], from: `\`${or[1]}\`` };
244
+ return null;
245
+ };
246
+ // El número de un valor: literal o constante. `via` es el nombre de la constante.
247
+ const numberOf = (v) => {
248
+ if (NUMBER.test(v.text)) return { value: Number(v.text), via: null };
249
+ const c = IDENT.test(v.text) && constant(v.text);
250
+ return c && NUMBER.test(c.text) ? { value: Number(c.text), via: v.text } : null;
251
+ };
252
+
253
+ // Entradas de primer nivel de un nodo: { key, sep, keyAt, value }. En JS, `clave: valor`, `'clave': valor` y la forma
254
+ // corta `{ temperature }` (sep null); en Python, `"clave": valor` en diccionarios y `clave=valor` en argumentos.
255
+ const entriesOf = (node) => {
256
+ const sep = node.open === '{' ? ':' : '=';
257
+ const out = [];
258
+ const push = (a, b) => {
259
+ const s = separator(code, a, b, sep);
260
+ if (s < 0) {
261
+ const v = trimmed(a, b);
262
+ if (!python && sep === ':' && IDENT.test(v.text)) out.push({ key: v.text, sep: null, keyAt: v.at, value: v });
263
+ return;
264
+ }
265
+ const k = trimmed(a, s);
266
+ const key = IDENT.test(k.text) ? k.text : literal(k);
267
+ if (key !== null) out.push({ key, sep, keyAt: k.at, value: trimmed(s + 1, b) });
268
+ };
269
+ let level = 0;
270
+ let a = node.start + 1;
271
+ for (let i = node.start + 1; i < node.end; i++) {
272
+ const c = code[i];
273
+ if ('([{'.includes(c)) level++;
274
+ else if (')]}'.includes(c)) level--;
275
+ else if (c === ',' && level === 0) { push(a, i); a = i + 1; }
276
+ }
277
+ push(a, node.end);
278
+ return out;
279
+ };
280
+
281
+ // Los pedidos: nodo → 'sdk' o 'http'.
282
+ const requests = new Map();
283
+ const paramVars = new Set();
284
+ for (const call of nodes.filter((x) => x.open === '(' && MESSAGES_CALL.test(before(x)))) {
285
+ const comma = separator(code, call.start + 1, call.end, ',');
286
+ const first = trimmed(call.start + 1, comma < 0 ? call.end : comma);
287
+ if (python) requests.set(call, 'sdk');
288
+ else if (code[first.at] === '{') requests.set(nodeAt(first.at), 'sdk');
289
+ const name = first.text.replace(/^\*\*/, '');
290
+ if (IDENT.test(name)) paramVars.add(name);
291
+ }
292
+ const http = /api\.anthropic\.com/.test(source) && /v1\/messages/.test(source);
293
+ for (const node of nodes) {
294
+ if (node.open !== '{' || requests.has(node)) continue;
295
+ const head = before(node);
296
+ const target = head.match(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?::[^=;\n]*)?=\s*$/);
297
+ if ((target && paramVars.has(target[1])) || TYPED_PARAMS.test(head)) { requests.set(node, 'sdk'); continue; }
298
+ if (!http) continue;
299
+ let openai = false;
300
+ for (let p = node.parent; p && !openai; p = p.parent) openai = p.open === '(' && OPENAI_CALL.test(before(p));
301
+ if (!openai) requests.set(node, 'http');
302
+ }
303
+
304
+ const findings = [];
305
+ const usages = [];
306
+ const report = (at, message, op) => {
307
+ findings.push({ pack: PACK, file, line: lineOf(at), status: 'detect-only', message });
308
+ usages.push({ provider: 'ia', op, line: lineOf(at) });
309
+ };
310
+ const shownOf = (param, e) => (e.sep === '=' ? `${param}=${e.value.text}` : e.sep ? `${param}: ${e.value.text}` : param);
311
+
312
+ // API version `2026-10-21`: la que manda el archivo (`new`, `old` o null si no la nombra) y el SDK instalado.
313
+ const header = (() => {
314
+ if (!anthropic || !strings.some((s) => s.value.toLowerCase() === 'anthropic-version')) return null;
315
+ if (strings.some((s) => s.value === api.version)) return 'new';
316
+ return strings.some((s) => /^2023-0[16]-01$/.test(s.value)) ? 'old' : null;
317
+ })();
318
+ const sdk = api?.sdks[python ? 'pypi' : 'npm'];
319
+ const installed = sdk ? context?.versions?.[python ? 'pypi' : 'npm']?.[sdk.package] || null : null;
320
+ const whereOf = (v) => (v.source === 'declarada' ? `declarado como \`${v.range}\`` : v.source === 'node_modules' ? 'instalado en node_modules' : `según ${v.source}`);
321
+ const cap = (s) => s[0].toUpperCase() + s.slice(1);
322
+ const apiVersion = (via, byKey, model, flagged) => {
323
+ // Un archivo que fija `2023-06-01` eligió quedarse; por HTTP, la versión la elige el archivo.
324
+ if (!anthropic || header === 'old' || (via === 'http' && header !== 'new')) return;
325
+ const usable = (e) => !EMPTY.test(e.value.text);
326
+ const candidates = PARAMS.filter((p) => byKey.has(p) && !flagged.has(p) && usable(byKey.get(p))).map((param) => ({ param, e: byKey.get(param), extra: false }));
327
+ // Python: lo que va en `extra_body={...}` llega a la API sin pasar por la firma del método.
328
+ const xb = python && byKey.get('extra_body');
329
+ const xn = xb && code[xb.value.at] === '{' ? nodeAt(xb.value.at) : null;
330
+ if (xn) for (const e of entriesOf(xn)) if (PARAMS.includes(e.key) && !flagged.has(e.key) && usable(e)) candidates.push({ param: e.key, e, extra: true });
331
+ const older = model && api.older_models.some((m) => model.id.startsWith(m)) ? model.id : null;
332
+ const pkg = `\`${sdk.package}\``;
333
+ for (const { param, e, extra } of candidates) {
334
+ // En Python, un parámetro directo falla antes de llegar a la API: el SDK 1.0 lo sacó de la firma.
335
+ const direct = python && !extra && via === 'sdk';
336
+ const target = direct ? sdk.typeerror_from : sdk.first;
337
+ const reason = direct
338
+ ? `desde la ${sdk.typeerror_from}, el SDK de Python ya no tiene \`temperature\`, \`top_p\` ni \`top_k\` y pasarlos tira \`TypeError\` con cualquier modelo`
339
+ : `desde la ${sdk.first}, el SDK usa la API version \`${api.version}\`, y ${api.removed_es}`;
340
+ const rec = api.recommendation_es.replace('{param}', `\`${param}\``);
341
+ const shown = `\`${shownOf(param, e)}\`${extra ? ' (en `extra_body`)' : ''}`;
342
+ let message;
343
+ if (header === 'new') {
344
+ message = `${shown} ya no funciona: el archivo manda \`anthropic-version: ${api.version}\`, y ${api.removed_es}. ${cap(rec)}.`;
345
+ } else if (installed && atLeast(installed.version, target)) {
346
+ message = `${shown} ya no funciona con ${pkg} ${installed.version} (${whereOf(installed)}): ${reason}.`
347
+ + (direct && !atLeast(installed.version, sdk.first) ? ` Tampoco sirve pasarlo por \`extra_body\`: con la ${sdk.first} (del ${api.released_es}) devuelve un error 400.` : '')
348
+ + ` ${cap(rec)}.`;
349
+ } else if (installed?.source === 'declarada' && openRange(installed.range)) {
350
+ message = `${shown} falla en una instalación nueva: ${pkg} está declarado como \`${installed.range}\`, que ya deja instalar la ${sdk.first} (del ${api.released_es}). ${cap(reason)}. ${cap(rec)}, o fijá el SDK debajo de la ${target}.`;
351
+ } else {
352
+ const when = `${target}${target === sdk.first ? ` (del ${api.released_es})` : ''}`;
353
+ const alsoExtra = direct ? ` Y desde la ${sdk.first} (del ${api.released_es}) tampoco sirve pasarlo por \`extra_body\`: la API version \`${api.version}\` lo rechaza.` : '';
354
+ // Sin la versión no se puede decir que hoy funciona: puede que ya falle.
355
+ message = installed
356
+ ? `${shown} funciona con ${pkg} ${installed.version} (${whereOf(installed)}), pero va a fallar cuando lo subas a la ${when}: ${reason}.${alsoExtra} Antes de aceptar esa suba, ${rec}.`
357
+ : `${shown} falla con ${pkg} ${target} o posterior: ${reason}.${alsoExtra} Parche no encontró qué versión del SDK usa este repo: si ya es la ${target} o una posterior, el pedido ya falla. ${cap(rec)}.`;
358
+ }
359
+ if (!python && via === 'sdk' && header !== 'new' && TS.test(file)) message += ` ${api.typescript_es}`;
360
+ if (param === 'temperature' && NUMBER.test(e.value.text) && Number(e.value.text) === 0) message += ` ${rules.temperature_zero_es}`;
361
+ if (older) message += ` ${api.older_model_es.replace('{model}', `\`${older}\``)}`;
362
+ report(e.keyAt, message, `Anthropic ${param} (API version ${api.version})`);
363
+ }
364
+ };
365
+
366
+ for (const [node, via] of requests) {
367
+ if (!node) continue;
368
+ const byKey = new Map(entriesOf(node).map((e) => [e.key, e]));
369
+ if (via === 'http' && !byKey.has('messages') && !byKey.has('max_tokens')) continue;
370
+ const model = byKey.has('model') ? modelOf(byKey.get('model').value) : null;
371
+ const flagged = new Set(); // lo que ya avisó la regla de Claude 4.7: no se repite
372
+ if (model && rules.models.includes(model.id)) claude47(byKey, via, model, flagged);
373
+ apiVersion(via, byKey, model, flagged);
374
+ }
375
+ // Los pedidos salen en el orden del árbol (de afuera hacia adentro); el reporte va en el orden del archivo.
376
+ const byLine = (a, b) => a.line - b.line;
377
+ return { findings: findings.sort(byLine), usages: usages.sort(byLine) };
378
+
379
+ // Claude 4.7 y posteriores: el parámetro con un valor fuera de lo aceptado (y, en Haiku 5.5, los dos juntos).
380
+ function claude47(byKey, via, model, flagged) {
381
+ const override = rules.overrides?.[model.id] || {};
382
+ const scope = override.scope_es || rules.scope_es;
383
+ const accepted = { ...rules.accepted, ...override.accepted };
384
+ const modelText = `\`${model.id}\`${model.from ? ` (el valor por defecto de ${model.from})` : ''}`;
385
+ const tail = (params, zero) => `Recomendación oficial de Anthropic: ${rules.recommendation_es.replace('{params}', params.map((p) => `\`${p}\``).join(' y '))}.`
386
+ + (zero ? ` ${rules.temperature_zero_es}` : '') + (python && via === 'sdk' ? ` ${rules.python_sdk_es}` : '');
387
+ const present = [];
388
+ for (const param of PARAMS) {
389
+ const e = byKey.get(param);
390
+ const num = e && numberOf(e.value);
391
+ if (!num) continue;
392
+ present.push({ param, e });
393
+ const ok = accepted[param];
394
+ if (ok.min !== undefined && num.value >= ok.min && num.value <= ok.max) continue;
395
+ flagged.add(param);
396
+ const shown = shownOf(param, e) + (num.via ? ` (${num.value})` : '');
397
+ report(e.keyAt, `\`${shown}\` con el modelo ${modelText} da un error 400: ${scope}, \`${param}\` ${ok.es}. ${tail([param], param === 'temperature' && num.value === 0)}`,
398
+ `Anthropic ${param} (Claude 4.7+)`);
399
+ }
400
+ const together = override.exclusive || [];
401
+ if (together.length && !flagged.size && together.every((p) => present.some((x) => x.param === p))) {
402
+ const last = present.filter((x) => together.includes(x.param)).at(-1);
403
+ 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)}`,
404
+ `Anthropic ${together.join(' + ')} (Claude 4.7+)`);
405
+ for (const p of together) flagged.add(p);
406
+ }
407
+ }
408
+ }
package/src/report.js CHANGED
@@ -102,7 +102,9 @@ export function partialCovers(findings) {
102
102
  return [...new Set(findings.filter((f) => f.partial).map((f) => f.partial.covers))];
103
103
  }
104
104
 
105
- export function prBody({ pack, findings, changed, verification }, today = new Date()) {
105
+ // `ciApproval`: el PR lo abre GitHub Actions con el GITHUB_TOKEN por defecto. GitHub deja los workflows de ese PR
106
+ // esperando aprobación (docs de GitHub, "GITHUB_TOKEN"): sin avisarlo, el PR parece roto.
107
+ export function prBody({ pack, findings, changed, verification, ciApproval = false }, today = new Date()) {
106
108
  const fs = findings.filter((f) => f.pack === pack.id);
107
109
  const c = counts(fs);
108
110
  const out = [];
@@ -152,10 +154,13 @@ export function prBody({ pack, findings, changed, verification }, today = new Da
152
154
  } else {
153
155
  out.push('- Sin verificar (corré `parche pr --verify`).');
154
156
  }
157
+ if (ciApproval) {
158
+ out.push('- CI de este PR: lo abrió GitHub Actions con el `GITHUB_TOKEN`, así que GitHub deja sus workflows esperando aprobación. Alguien con permiso de escritura tiene que tocar **Approve workflows to run**. Para que corran solos, pasale a la Action un token de una GitHub App o un token personal (input `token`).');
159
+ }
155
160
  }
156
161
  out.push('', asIssue ? '### Checklist' : '### Checklist antes de mergear', '', ...pack.review_checklist_es.map((x) => `- [ ] ${x}`), '');
157
162
  out.push('### Fuentes', '', ...pack.sources.map((s) => `- [${s.title}](${s.url})`), '');
158
- out.push(`<sub>Pack \`${pack.id}\` v${pack.version}, verificado el ${pack.last_verified}. Resumen: ${Object.entries(c).map(([k, v]) => `${LABEL[k]} ${v}`).join(' · ')}. ¿Este PR te sirvió? Mergealo, editalo o cerralo: Parche aprende de cada resultado.</sub>`, '');
163
+ out.push(`<sub>Pack \`${pack.id}\` v${pack.version}, verificado el ${pack.last_verified}. Resumen: ${Object.entries(c).map(([k, v]) => `${LABEL[k]} ${v}`).join(' · ')}. ¿Este PR te sirvió? Mergealo, editalo o cerralo: Parche aprende de cada resultado. [¿Qué es Parche y por qué recibiste esto?](https://github.com/Pepo08/parche/blob/main/docs/por-que-recibi-este-pr.md)</sub>`, '');
159
164
  return out.join('\n');
160
165
  }
161
166
 
@@ -8,20 +8,25 @@ on:
8
8
  - cron: "17 11 * * 1" # lunes 8:17 (hora de Argentina)
9
9
  workflow_dispatch:
10
10
 
11
+ # Por defecto, solo lectura. La escritura la pide el único job que la necesita.
11
12
  permissions:
12
- contents: write
13
- pull-requests: write
13
+ contents: read
14
14
 
15
15
  jobs:
16
16
  parche:
17
17
  runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: write # pushear la rama parche/<pack>
20
+ pull-requests: write # abrir el PR
18
21
  steps:
19
- - uses: actions/checkout@v7
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20
23
  with:
21
24
  fetch-depth: 0
22
- - uses: actions/setup-node@v7
25
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
23
26
  with:
24
27
  node-version: 24
25
28
  - name: Instalar dependencias del proyecto (para correr sus tests antes de cada PR)
26
29
  run: npm ci
30
+ # v0 se mueve con cada release 0.x. Para fijar una versión, cambiá v0 por el commit de ese release
31
+ # (https://github.com/Pepo08/parche/releases), por ejemplo: Pepo08/parche@<sha> # v0.6.7
27
32
  - uses: Pepo08/parche@v0