parche-cli 0.6.7 → 0.6.9

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,34 @@
2
2
 
3
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
4
 
5
+ ## 0.6.9 — 2026-10-23
6
+
7
+ La Action muestra lo que no abre un PR, y la plantilla del workflow anda en repos PHP. Salió de probar cada comando sobre los ejemplos antes de mandar los primeros mensajes de venta.
8
+
9
+ ### Nuevo
10
+ - **Resumen del job y anotaciones en GitHub Actions:** `parche pr` escribe en el resumen de la corrida (`$GITHUB_STEP_SUMMARY`) los PRs que abrió, lo que quedó sin PR (por ejemplo, una factura A a un CUIT, donde falta la condición frente al IVA del cliente) y los avisos de los packs de solo detección, como un modelo de IA que se retira. Las primeras 10 también salen como anotaciones con el archivo y la línea. Hasta la 0.6.8, `pr` salteaba los packs sin arreglo y la Action no mostraba los avisos de IA.
11
+ - **Fuera de GitHub Actions,** `parche pr` imprime al final los avisos de solo detección ("Avisos sin PR").
12
+
13
+ ### Cambios
14
+ - **Plantilla del workflow (`templates/github-workflow.yml`):** `npm ci` corre solo si hay `package-lock.json`, y `composer install` solo si hay `composer.json`. Antes, en un repo PHP el workflow fallaba en el primer paso.
15
+ - **Inventario en PHP y Python:** un SDK declarado en `composer.json` o `requirements.txt` dice "los usos en PHP (o Python) todavía no se analizan", en vez de "sin usos encontrados", que sonaba a que no se usa.
16
+ - La CI prueba la Action también sobre un repo PHP (copia de `examples/tienda-php`).
17
+
18
+ ## 0.6.8 — 2026-10-22
19
+
20
+ Pack de IA: los alias de Claude que apuntan a un modelo con fecha. Antes de la generación 4.6, la API de Claude acepta alias cortos que apuntan al snapshot fechado más reciente de cada versión (página "Model IDs and versioning", verificada el 22/10/2026). La 0.6.7 avisaba `claude-sonnet-4-5-20250929`, que se retira el 30/11/2026, pero no `claude-sonnet-4-5`, que es el mismo modelo.
21
+
22
+ ### Nuevo
23
+ - **`claude-sonnet-4-5`:** se avisa como su snapshot, con la fecha (30/11/2026) y el reemplazo que recomienda Anthropic (`claude-sonnet-5-5`). El aviso nombra el snapshot: "El modelo `claude-sonnet-4-5` (alias de `claude-sonnet-4-5-20250929`) se retira el 2026-11-30".
24
+ - **`claude-opus-4-5` y `claude-haiku-4-5`:** se avisan con la fecha mínima de retiro de su snapshot, igual que los IDs fechados.
25
+ - Los alias viven en el pack (`model_aliases`), cada uno con la página del modelo como fuente: sumar otro es agregar una línea.
26
+
27
+ ### Sin cambios
28
+ - Los IDs de Amazon Bedrock (`anthropic.claude-…`) y de Google Cloud (`claude-sonnet-4-5@20250929`) no se marcan: esas plataformas tienen su propio calendario de retiros.
29
+
30
+ ### Todavía no cubre
31
+ - Los alias de modelos ya retirados (`claude-3-5-sonnet-latest`, `claude-3-7-sonnet-latest` y otros): la documentación actual de Anthropic ya no los nombra, y sin fuente no se avisan.
32
+
5
33
  ## 0.6.7 — 2026-10-21
6
34
 
7
35
  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).
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Joaquín González
3
+ Copyright (c) 2026 Joaquin Gonzalez Menza
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -91,7 +91,9 @@ Para que corra todas las semanas, copiá [`templates/github-workflow.yml`](templ
91
91
  # dry-run: 'true' # opcional: armar las ramas sin abrir PRs
92
92
  ```
93
93
 
94
- El workflow necesita `permissions: contents: write` y `pull-requests: write`.
94
+ El workflow necesita `permissions: contents: write` y `pull-requests: write`. Antes de Parche, la plantilla instala las dependencias de tu proyecto para que corran sus tests: `npm ci` si hay `package-lock.json` y `composer install` si hay `composer.json` (Composer viene en los runners de Ubuntu de GitHub). Así anda igual en un repo de Node y en uno de PHP.
95
+
96
+ Lo que no abre un PR también se ve en la corrida: los avisos de solo detección (modelos de IA que se retiran, por ejemplo) y lo que quedó sin PR porque falta un dato de tu negocio aparecen en el **resumen del job** y como **anotaciones** con el archivo y la línea (GitHub muestra las primeras 10; el resumen las tiene todas).
95
97
 
96
98
  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
99
 
@@ -130,7 +132,7 @@ Cómo contribuir, las reglas para el código y cómo se saca una versión: [`CON
130
132
  |---|---|---|---|---|
131
133
  | `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) |
132
134
  | `mercadopago.payments-to-orders` | Mercado Pago | migración | — | sí (creación de pagos en SDK Node v2) |
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
+ | `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 o con fecha, también por su alias (`claude-sonnet-4-5`, que apunta a `claude-sonnet-4-5-20250929` y se retira el 30/11/2026), `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 |
134
136
 
135
137
  ## Verificación
136
138
 
@@ -204,6 +206,7 @@ Cuando `DocTipo` no es un número fijo (`DocTipo: docTipo`), Parche no puede sab
204
206
  - 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
207
  - 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
208
  - 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.
209
+ - IA, alias de Anthropic: avisa los de Claude Sonnet 4.5, Opus 4.5 y Haiku 4.5 (`claude-sonnet-4-5`, `claude-opus-4-5`, `claude-haiku-4-5`), escritos enteros entre comillas. Los alias de modelos ya retirados (`claude-3-5-sonnet-latest`, `claude-3-7-sonnet-latest` y otros) todavía no: la documentación actual de Anthropic ya no los nombra.
207
210
  - 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.
208
211
  - 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.
209
212
  - 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
@@ -9,8 +9,9 @@ import { verify } from '../src/verify.js';
9
9
  import * as git from '../src/git.js';
10
10
  import { parseGithubRemote, openPullRequest } from '../src/github.js';
11
11
  import { record } from '../src/ledger.js';
12
- import { apiLabel } from '../src/inventory/index.js';
12
+ import { apiLabel, noUsesLabel } from '../src/inventory/index.js';
13
13
  import { isTestFile } from '../src/paths.js';
14
+ import { stepSummary, annotations, byPack, workspacePrefix } from '../src/actions.js';
14
15
 
15
16
  const HELP = `parche — Dependabot para APIs
16
17
 
@@ -67,7 +68,7 @@ function printInventory(inventory) {
67
68
  const ops = api.operations.map((o) => (o.count > 1 ? `${o.op} ×${o.count}` : o.op));
68
69
  const shown = ops.length > 4 ? [...ops.slice(0, 4), `+${ops.length - 4} más`] : ops;
69
70
  const packs = api.packs.length ? ' · con pack' : '';
70
- console.log(` ${KIND[api.kind]} ${apiLabel(api).padEnd(38)} ${api.confidence.padEnd(5)} ${shown.join(', ') || '(declarada, sin usos encontrados)'}${packs}`);
71
+ console.log(` ${KIND[api.kind]} ${apiLabel(api).padEnd(38)} ${api.confidence.padEnd(5)} ${shown.join(', ') || `(${noUsesLabel(api)})`}${packs}`);
71
72
  }
72
73
  if (inventory.unresolved.length) {
73
74
  console.log(` + ${inventory.unresolved.length} llamada(s) HTTP con URL dinámica (no se sabe a qué host van; detalle en el reporte)`);
@@ -225,6 +226,9 @@ async function main() {
225
226
  const packs = loadPacks().filter((p) => p.codemod && (!packIds || packIds.includes(p.id)));
226
227
  // A dónde se pushea: origin, o el fork de quien abre el PR (--push-remote fork). El PR se abre contra origin.
227
228
  const pushRemote = typeof args['push-remote'] === 'string' ? args['push-remote'] : 'origin';
229
+ // Para el resumen del job en GitHub Actions: los PRs de esta corrida y lo que quedó sin PR.
230
+ const opened = [];
231
+ const withoutPr = [];
228
232
  for (const pack of packs) {
229
233
  const preview = run(root, { write: false, packIds: [pack.id] });
230
234
  if (!preview.findings.length) continue;
@@ -258,6 +262,7 @@ async function main() {
258
262
  console.log(` ${summary(result.findings)}`);
259
263
  console.log(` ${why[0].toUpperCase()}${why.slice(1)}; queda como issue o reporte: ${bodyPath}`);
260
264
  record(root, { type: 'report-only', pack: pack.id, findings: c, reason: why });
265
+ withoutPr.push({ pack, findings: result.findings });
261
266
  continue;
262
267
  }
263
268
  const draft = !!(c['needs-review'] || c['needs-input'] || c.blocked);
@@ -277,6 +282,7 @@ async function main() {
277
282
  } else {
278
283
  console.log(` Modo prueba: no se abrió el PR${draft ? ' (iría como borrador por tener ítems para revisar)' : ''}.`);
279
284
  }
285
+ opened.push({ title, url, draft });
280
286
  record(root, { type: 'pr', pack: pack.id, packVersion: pack.version, branch, sha, url, draft, files: changedFiles, findings: c,
281
287
  partial: result.findings.filter((f) => f.partial).length });
282
288
  } finally {
@@ -285,6 +291,19 @@ async function main() {
285
291
  if (!keepBranch) git.deleteBranch(root, branch);
286
292
  }
287
293
  }
294
+ // Los packs de solo detección (IA) no abren PR: sus avisos se muestran acá y, en GitHub Actions, en el resumen del
295
+ // job y como anotaciones, junto con lo que quedó sin PR. Antes `pr` los salteaba y en la Action no se veían (D67).
296
+ const detectPacks = loadPacks().filter((p) => !p.codemod && (!packIds || packIds.includes(p.id)));
297
+ const detected = detectPacks.length ? run(root, { write: false, packIds: detectPacks.map((p) => p.id) }) : { packs: [], findings: [] };
298
+ const notices = byPack(detected);
299
+ if (notices.length) {
300
+ console.log('\nAvisos sin PR (solo detección):');
301
+ printFindings(detected);
302
+ }
303
+ if (process.env.GITHUB_ACTIONS === 'true') {
304
+ if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, stepSummary({ opened, reportOnly: withoutPr, notices }));
305
+ for (const line of annotations([...notices, ...withoutPr], workspacePrefix(root, process.env.GITHUB_WORKSPACE))) console.log(line);
306
+ }
288
307
  return 0;
289
308
  }
290
309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
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",
@@ -25,7 +25,7 @@
25
25
  "type": "git",
26
26
  "url": "git+https://github.com/Pepo08/parche.git"
27
27
  },
28
- "author": "Joaquín González",
28
+ "author": "Joaquin Gonzalez Menza",
29
29
  "license": "MIT",
30
30
  "type": "module",
31
31
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "ai.model-retirements",
3
- "version": "2026.10.21",
3
+ "version": "2026.10.22",
4
4
  "provider": "Modelos de IA",
5
5
  "title": "Modelos, endpoints y parámetros de IA retirados o por retirarse",
6
6
  "kind": "retirement",
@@ -11,6 +11,10 @@
11
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 IDs and versioning (antes de la generación 4.6, el alias apunta al snapshot fechado más reciente)", "url": "https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions" },
15
+ { "title": "Anthropic: Claude Sonnet 4.5 (alias claude-sonnet-4-5; se retira el 30/11/2026)", "url": "https://platform.claude.com/docs/en/models/sonnet-4-5/overview" },
16
+ { "title": "Anthropic: Claude Opus 4.5 (alias claude-opus-4-5)", "url": "https://platform.claude.com/docs/en/models/opus-4-5/overview" },
17
+ { "title": "Anthropic: Claude Haiku 4.5 (alias claude-haiku-4-5)", "url": "https://platform.claude.com/docs/en/models/haiku-4-5/overview" },
14
18
  { "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
19
  { "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
20
  { "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" },
@@ -170,6 +174,11 @@
170
174
  "ft-babbage-002": { "date": "2026-10-23", "replacement": "gpt-5.6-terra", "bases": ["babbage-002"] },
171
175
  "ft-davinci-002": { "date": "2026-10-23", "replacement": "gpt-5.6-terra", "bases": ["davinci-002"] }
172
176
  },
177
+ "model_aliases": {
178
+ "claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
179
+ "claude-opus-4-5": "claude-opus-4-5-20251101",
180
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001"
181
+ },
173
182
  "retirement_floors": {
174
183
  "claude-haiku-4-5-20251001": "2026-10-15",
175
184
  "claude-opus-4-5-20251101": "2026-11-24"
package/src/actions.js ADDED
@@ -0,0 +1,72 @@
1
+ // Lo que se ve en GitHub Actions además de los PRs: el resumen del job ($GITHUB_STEP_SUMMARY) y las anotaciones
2
+ // (::warning) con archivo y línea. Sin esto, los avisos de los packs de solo detección (IA) y lo que queda sin PR
3
+ // (falta un dato, o no hay código para cambiar) se perdían dentro del runner (sesión del 23/10, D67).
4
+ import path from 'node:path';
5
+ import { LABEL } from './report.js';
6
+
7
+ // Comandos de workflow de GitHub: el mensaje escapa %, \r y \n; las propiedades (file, title), además, ":" y ",".
8
+ export const escapeData = (s) => String(s).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
9
+ export const escapeProperty = (s) => escapeData(s).replace(/:/g, '%3A').replace(/,/g, '%2C');
10
+
11
+ // GitHub muestra hasta 10 anotaciones de advertencia por paso: las primeras van como anotación y todas en el resumen.
12
+ export const MAX_ANNOTATIONS = 10;
13
+
14
+ // Grupos { pack, findings } de un resultado del motor, en el orden de los packs y sin los packs vacíos.
15
+ export const byPack = (result) => result.packs
16
+ .map((pack) => ({ pack, findings: result.findings.filter((f) => f.pack === pack.id) }))
17
+ .filter((g) => g.findings.length);
18
+
19
+ // Prefijo para que la ruta del hallazgo (relativa a la carpeta que se revisó) sea relativa a la raíz del checkout,
20
+ // que es lo que espera una anotación. Si la carpeta está fuera del checkout, no hay prefijo válido: null.
21
+ export function workspacePrefix(root, workspace) {
22
+ if (!workspace) return '';
23
+ const rel = path.relative(path.resolve(workspace), path.resolve(root));
24
+ if (rel === '') return '';
25
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
26
+ return `${rel.split(path.sep).join('/')}/`;
27
+ }
28
+
29
+ // Una anotación por hallazgo, hasta MAX_ANNOTATIONS. Sin prefijo válido, el archivo y la línea van en el mensaje.
30
+ export function annotations(groups, prefix = '') {
31
+ const out = [];
32
+ for (const { pack, findings } of groups) {
33
+ for (const f of findings) {
34
+ if (out.length === MAX_ANNOTATIONS) return out;
35
+ const where = `${f.file}${f.line ? `:${f.line}` : ''}`;
36
+ const props = prefix === null ? [] : [`file=${escapeProperty(prefix + f.file)}`, ...(f.line ? [`line=${f.line}`] : [])];
37
+ props.push(`title=${escapeProperty(`Parche · ${pack.title}`)}`);
38
+ out.push(`::warning ${props.join(',')}::${escapeData(prefix === null ? `${where}: ${f.message}` : f.message)}`);
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+
44
+ // El resumen del job: los PRs de esta corrida, lo que quedó sin PR y los avisos de solo detección.
45
+ // opened: [{ title, url, draft }] (url null en modo prueba); reportOnly y notices: grupos { pack, findings }.
46
+ export function stepSummary({ opened = [], reportOnly = [], notices = [] }) {
47
+ const lines = ['## Parche', ''];
48
+ if (opened.length) {
49
+ lines.push('### PRs', '');
50
+ for (const o of opened) {
51
+ const draft = o.draft ? ' · borrador' : '';
52
+ lines.push(o.url ? `- [${o.title}](${o.url})${draft}` : `- ${o.title}${draft} · modo prueba: no se abrió`);
53
+ }
54
+ lines.push('');
55
+ }
56
+ const section = (title, intro, groups) => {
57
+ if (!groups.length) return;
58
+ lines.push(`### ${title}`, '', intro, '');
59
+ for (const { pack, findings } of groups) {
60
+ lines.push(`**${pack.title}** · ${pack.provider}`, '');
61
+ for (const f of findings) lines.push(`- ${LABEL[f.status]} · \`${f.file}${f.line ? `:${f.line}` : ''}\` · ${f.message}`);
62
+ lines.push('');
63
+ }
64
+ };
65
+ section('Sin PR', 'Parche no abre un PR cuando el arreglo depende de un dato de tu negocio o cuando no hay código para cambiar. Esto es lo que encontró y lo que falta.', reportOnly);
66
+ section('Avisos', 'Cambios con fecha que afectan a tu código. Parche no los cambia solo: cambiar de modelo, por ejemplo, cambia cómo responde tu producto.', notices);
67
+ if (!opened.length && !reportOnly.length && !notices.length) {
68
+ lines.push('Ninguno de los packs verificados tiene cambios que te afecten hoy.', '');
69
+ }
70
+ lines.push('El inventario completo de las APIs del repo: `npx parche-cli scan .`');
71
+ return `${lines.join('\n')}\n`;
72
+ }
@@ -28,6 +28,17 @@ const entry = (v) => (typeof v === 'string' ? { date: v, replacement: null } : v
28
28
  const quoted = (line, id) => line.includes(`'${id}'`) || line.includes(`"${id}"`) || line.includes(`\`${id}\``);
29
29
  const replacementText = ({ replacement, replacement_es: prose, note_es: note }) =>
30
30
  (replacement ? ` Reemplazo recomendado: \`${replacement}\`.` : prose ? ` Reemplazo recomendado: ${prose}.` : '') + (note ? ` ${note}` : '');
31
+ // `aliasOf`: el snapshot al que apunta un alias de Anthropic (`model_aliases` del pack). Sin alias, el texto es el de siempre.
32
+ const retiredMessage = (model, e, inCall, today, aliasOf) => {
33
+ const past = e.date <= today;
34
+ const repl = replacementText(e);
35
+ const when = past ? `fue retirado el ${e.date}` : `se retira el ${e.date}`;
36
+ return inCall
37
+ ? `El modelo \`${model}\`${aliasOf ? ` (alias de \`${aliasOf}\`)` : ''} ${when}${past ? ': las llamadas ya fallan' : ''}.${repl} Corré tus pruebas de prompts antes de cambiarlo.`
38
+ : `Aparece \`${model}\` (${aliasOf ? `alias de \`${aliasOf}\`, ` : ''}${when}) fuera de una llamada: si es una lista o un catálogo de modelos, actualizalo.${repl}`;
39
+ };
40
+ const floorMessage = (model, date, aliasOf) =>
41
+ `El modelo \`${model}\`${aliasOf ? ` (alias de \`${aliasOf}\`)` : ''} puede retirarse a partir del ${date} (fecha mínima publicada; hay 60 días de aviso). Planificá el reemplazo.`;
31
42
  const endpointHit = (ep, l) => {
32
43
  if (ep.pattern) return l.includes(ep.pattern) ? ep.pattern : null;
33
44
  const m = l.match(ep.re);
@@ -41,7 +52,7 @@ const milestonesText = (ep, today) => (ep.milestones || [])
41
52
  export default function detect({ source, file, pack, today = new Date().toISOString().slice(0, 10), context }) {
42
53
  const findings = [];
43
54
  const usages = [];
44
- const { retired_models: retired, retirement_floors: floors, retired_endpoints: endpoints } = pack.detect;
55
+ const { retired_models: retired, retirement_floors: floors, retired_endpoints: endpoints, model_aliases: aliases = {} } = pack.detect;
45
56
  const fineTunes = new Map();
46
57
  for (const [name, e] of Object.entries(pack.detect.retired_fine_tunes || {})) for (const b of e.bases) fineTunes.set(b, { name, ...e });
47
58
  const azure = AZURE.test(source);
@@ -53,17 +64,22 @@ export default function detect({ source, file, pack, today = new Date().toISOStr
53
64
  const inCall = MODEL_CONTEXT.test(l);
54
65
  for (const [model, raw] of Object.entries(retired)) {
55
66
  if (!quoted(l, model)) continue;
56
- const e = entry(raw);
57
- const { date } = e;
58
- const past = date <= today;
59
- const repl = replacementText(e);
60
- const when = past ? `fue retirado el ${date}` : `se retira el ${date}`;
61
- const message = inCall
62
- ? `El modelo \`${model}\` ${when}${past ? ': las llamadas ya fallan' : ''}.${repl} Corré tus pruebas de prompts antes de cambiarlo.`
63
- : `Aparece \`${model}\` (${when}) fuera de una llamada: si es una lista o un catálogo de modelos, actualizalo.${repl}`;
64
- findings.push({ pack: PACK, file, line, status: 'detect-only', message });
67
+ findings.push({ pack: PACK, file, line, status: 'detect-only', message: retiredMessage(model, entry(raw), inCall, today) });
65
68
  usages.push({ provider: 'ia', op: `modelo ${model}`, line });
66
69
  }
70
+ // Alias de Anthropic de antes de la generación 4.6 (`claude-sonnet-4-5`): apuntan al snapshot fechado más reciente y
71
+ // tienen su fecha (Model IDs and versioning). Se comparan enteros entre comillas: `'claude-sonnet-4-5'` no es
72
+ // `'claude-sonnet-4-5-20250929'` (ese ya se avisa arriba), ni el ID de Bedrock (`anthropic.claude-…`) o el de Google
73
+ // Cloud (`claude-sonnet-4-5@20250929`), que tienen su propio calendario.
74
+ for (const [alias, target] of Object.entries(aliases)) {
75
+ if (!quoted(l, alias)) continue;
76
+ let message;
77
+ if (retired[target]) message = retiredMessage(alias, entry(retired[target]), inCall, today, target);
78
+ else if (floors[target]) message = floorMessage(alias, floors[target], target);
79
+ else continue;
80
+ findings.push({ pack: PACK, file, line, status: 'detect-only', message });
81
+ usages.push({ provider: 'ia', op: `modelo ${alias}`, line });
82
+ }
67
83
  for (const [, base] of l.matchAll(FINE_TUNE)) {
68
84
  const ft = fineTunes.get(base);
69
85
  if (!ft) continue;
@@ -78,8 +94,7 @@ export default function detect({ source, file, pack, today = new Date().toISOStr
78
94
  }
79
95
  for (const [model, date] of Object.entries(floors)) {
80
96
  if (!l.includes(model)) continue;
81
- findings.push({ pack: PACK, file, line, status: 'detect-only',
82
- message: `El modelo \`${model}\` puede retirarse a partir del ${date} (fecha mínima publicada; hay 60 días de aviso). Planificá el reemplazo.` });
97
+ findings.push({ pack: PACK, file, line, status: 'detect-only', message: floorMessage(model, date) });
83
98
  usages.push({ provider: 'ia', op: `modelo ${model}`, line });
84
99
  }
85
100
  for (const ep of eps) {
@@ -126,6 +126,14 @@ export function buildInventory({ repo, manifests, fileScans, packs }) {
126
126
  }
127
127
 
128
128
  // Nombre corto para mostrar una API en el reporte y en la CLI.
129
+ // Qué decir de un SDK sin operaciones: en JS/TS se buscaron los usos y no aparecieron; en PHP y Python los usos todavía
130
+ // no se buscan (solo se lee el manifiesto), y "sin usos encontrados" sonaba a que no se usa (sesión del 23/10).
131
+ const NOT_SCANNED = { composer: ['composer.json', 'PHP'], pypi: ['requirements.txt', 'Python'] };
132
+ export function noUsesLabel(api) {
133
+ const ns = api.kind === 'sdk' && NOT_SCANNED[api.ecosystem];
134
+ return ns ? `declarada en ${ns[0]}; los usos en ${ns[1]} todavía no se analizan` : 'declarada, sin usos encontrados';
135
+ }
136
+
129
137
  export function apiLabel(api) {
130
138
  if (api.kind === 'sdk') return api.provider ? `${api.provider} · ${api.package}` : api.package;
131
139
  if (api.kind === 'http') return api.provider ? `${api.provider} · ${api.host}` : api.host;
package/src/report.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Reporte del scan y cuerpo del PR, en castellano.
2
- import { apiLabel } from './inventory/index.js';
2
+ import { apiLabel, noUsesLabel } from './inventory/index.js';
3
3
 
4
4
  const LABEL ={
5
5
  auto: '✅ automático',
@@ -62,7 +62,7 @@ export function scanReport({ inventory, findings, packs, changed, upgrades = nul
62
62
  const KIND = { sdk: 'SDK', http: 'HTTP directo', 'ai-model': 'Modelos de IA' };
63
63
  out.push('| Tipo | API | Rubro | Confianza | Operaciones (usos) | Pack |', '|---|---|---|---|---|---|');
64
64
  for (const api of inventory.apis) {
65
- const ops = api.operations.map((o) => `\`${o.op}\` ×${o.count}`).join(', ') || 'declarada, sin usos encontrados';
65
+ const ops = api.operations.map((o) => `\`${o.op}\` ×${o.count}`).join(', ') || noUsesLabel(api);
66
66
  out.push(`| ${KIND[api.kind]} | ${apiLabel(api)} | ${api.category || '—'} | ${api.confidence} | ${ops} | ${api.packs.length ? 'sí' : '—'} |`);
67
67
  }
68
68
  out.push('');
@@ -25,8 +25,16 @@ jobs:
25
25
  - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
26
26
  with:
27
27
  node-version: 24
28
- - name: Instalar dependencias del proyecto (para correr sus tests antes de cada PR)
28
+ # Las dependencias del proyecto, para correr sus tests antes de cada PR. Cada paso corre solo si el repo tiene
29
+ # ese archivo: en un repo PHP no hay package-lock.json y `npm ci` fallaría.
30
+ - name: Instalar dependencias de Node
31
+ if: hashFiles('package-lock.json') != ''
29
32
  run: npm ci
33
+ # Composer viene instalado en los runners de Ubuntu de GitHub. Si tu proyecto pide otra versión de PHP,
34
+ # agregá antes un paso que la instale.
35
+ - name: Instalar dependencias de PHP
36
+ if: hashFiles('composer.json') != ''
37
+ run: composer install --no-interaction --no-progress
30
38
  # v0 se mueve con cada release 0.x. Para fijar una versión, cambiá v0 por el commit de ese release
31
39
  # (https://github.com/Pepo08/parche/releases), por ejemplo: Pepo08/parche@<sha> # v0.6.7
32
40
  - uses: Pepo08/parche@v0