parche-cli 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
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.7.0 — 2026-10-24
6
+
7
+ ARCA: Parche encuentra a los repos que creen estar migrados y no lo están, reconoce `@arcasdk/core` (la segunda librería de ARCA más descargada en npm) y avisa de la librería vieja en más casos. Salió de los pendientes del escaneo de 174 repos públicos (frente B, puntos 12 a 14).
8
+
9
+ ### Nuevo
10
+ - **Clave casi bien escrita:** si el detalle del comprobante lleva `CondicionIVAReceptor` (sin `Id`), `CondicionIvaReceptorId` o `condicionIVAReceptorId`, Parche la renombra a `CondicionIVAReceptorId` sin tocar el valor, en JS y en PHP. Ese elemento no existe en el WSDL de WSFEv1: el dato nunca llegaba y desde el 1/12/2026 ARCA rechaza el CAE. Pasaba en un repo real con integración SOAP propia, en cuatro archivos. El PR sale borrador, para revisar que el valor sea un código válido. Solo se toca dentro de objetos o arrays con al menos dos claves de WSFEv1 (`DocTipo`, `CbteDesde`, `ImpTotal`…), nunca en tests ni en modelos propios con un nombre parecido. Si el comprobante tiene las dos claves, no edita y pide revisar.
11
+ - **`@arcasdk/core`:** `arca.electronicBillingService.createVoucher(...)` y `createNextVoucher(...)` reciben los mismos arreglos que `afip.js` (consumidor final, DNI con B o C, PR parcial). Su comprobante usa las mismas claves de WSFEv1 (tipos de la 2.0.2, leídos en el paquete de npm). Probado sobre cinco repos públicos que la usan.
12
+ - **Guía "Cómo agregar `CondicionIVAReceptorId` antes del 1/12/2026"** (`docs/guia-condicion-iva-receptor.md`): qué cambia, el campo en afip.js, afip.php, @arcasdk/core y SOAP propio, los códigos por clase de comprobante, los tres errores que aparecieron en repos reales y cómo probarlo en homologación. Para quien prefiere hacerlo a mano.
13
+
14
+ ### Cambios
15
+ - **`ts-afip-ws`:** se avisa como librería vieja. Es un fork de afip.js 0.x y su última versión (1.3.2, de 2023) trae `wsfe.wsdl` y `wsfe-production.wsdl` sin el campo. El aviso dice qué hacer: pasar a `@afipsdk/afip.js` 1.x o actualizar el WSDL que trae la librería (lo mismo para `eesnaola/afip.php`, que no tiene una 1.x).
16
+ - **El aviso de librería vieja también sale cuando Parche no puede leer los datos del comprobante** (por ejemplo, si llegan de otra función). Antes salía solo en los arreglos automáticos y en lo que pedía un dato con el objeto a la vista.
17
+ - **Cuerpo del issue:** el pie dice "¿Te sirvió este aviso?" en vez de "¿Este PR te sirvió?".
18
+ - **Checklist:** "confirmar que cada cliente con CUIT tiene su condición" sale solo si queda algo pendiente. En un PR que arregla todo, como uno de puros consumidores finales, no aplicaba (nuevo campo de pack: `review_checklist_if_pending_es`).
19
+ - **Pack `arca.wsfe.condicion-iva-receptor` 2026.10.24:** fuentes releídas el 24/10/2026 (sin prórroga del 1/12); la explicación ya no cita una versión del manual de WSFEv1, que la página de ARCA rotula distinto; suma `facturas` (LaPyme) y las llamadas de `@arcasdk/core` a la detección.
20
+
21
+ ## 0.6.9 — 2026-10-23
22
+
23
+ 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.
24
+
25
+ ### Nuevo
26
+ - **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.
27
+ - **Fuera de GitHub Actions,** `parche pr` imprime al final los avisos de solo detección ("Avisos sin PR").
28
+
29
+ ### Cambios
30
+ - **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.
31
+ - **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.
32
+ - La CI prueba la Action también sobre un repo PHP (copia de `examples/tienda-php`).
33
+
5
34
  ## 0.6.8 — 2026-10-22
6
35
 
7
36
  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.
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
 
@@ -128,7 +130,7 @@ Cómo contribuir, las reglas para el código y cómo se saca una versión: [`CON
128
130
 
129
131
  | Pack | Proveedor | Tipo | Fecha límite | Arreglo |
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
+ | `arca.wsfe.condicion-iva-receptor` | ARCA | rompe | 2026-12-01 | sí, en JS (`afip.js` y `@arcasdk/core`) 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
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
 
@@ -157,6 +159,8 @@ Para reportar una vulnerabilidad, y el detalle de todo esto: [`SECURITY.md`](SEC
157
159
 
158
160
  ## ARCA en PHP
159
161
 
162
+ **¿Tenés que agregar el campo a mano?** La guía paso a paso, para afip.js, afip.php, @arcasdk/core y SOAP propio: [Cómo agregar `CondicionIVAReceptorId` antes del 1/12/2026](docs/guia-condicion-iva-receptor.md).
163
+
160
164
  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:
161
165
 
162
166
  ```php
@@ -169,7 +173,9 @@ Para `afipsdk/afip.php` y sus forks (`fertandil87/afip.php`, `ivanalemunioz/afip
169
173
  - Copia el estilo del array: comillas de las claves, indentación (espacios o tabs), alineación de los `=>`, coma final y fin de línea (LF o CRLF).
170
174
  - Saltea `vendor/`.
171
175
 
172
- **La versión de la librería importa.** Con `afip.js`/`afip.php` 0.x (y `eesnaola/afip.php`) el pedido se arma con un WSDL de 2021 que no tiene `CondicionIVAReceptorId`: aunque el campo esté en el código, no llega a ARCA. Parche lee la versión instalada (`composer.lock`, `package-lock.json`, `node_modules`) y, en esos casos, el arreglo sale como **"revisar"** con el aviso de actualizar a 1.x. También avisa si el repo tiene un `.wsdl` de WSFEv1 sin el campo.
176
+ **La versión de la librería importa.** Con `afip.js`/`afip.php` 0.x, `eesnaola/afip.php` y `ts-afip-ws` (un fork de afip.js 0.x; su última versión, la 1.3.2, trae el WSDL sin el campo) el pedido se arma con un WSDL de 2021 que no tiene `CondicionIVAReceptorId`: aunque el campo esté en el código, no llega a ARCA. Parche lee la versión instalada (`composer.lock`, `package-lock.json`, `node_modules`) y, en esos casos, el arreglo sale como **"revisar"** y lo que pide un dato lleva el mismo aviso: qué actualizar para que el campo viaje. También avisa si el repo tiene un `.wsdl` de WSFEv1 sin el campo.
177
+
178
+ **Clave casi bien escrita.** Si el detalle del comprobante lleva `CondicionIVAReceptor` (sin `Id`), `CondicionIvaReceptorId` o `condicionIVAReceptorId`, ese elemento no existe en el WSDL: el dato nunca llega y el repo parece migrado. Parche renombra la clave a `CondicionIVAReceptorId` sin tocar el valor, en JS y en PHP, y el PR sale **borrador** para revisar que el valor sea un código válido. Solo lo hace dentro de objetos o arrays con al menos dos claves de WSFEv1 (`DocTipo`, `CbteDesde`, `ImpTotal`…), para no tocar modelos propios con un nombre parecido, y nunca en tests.
173
179
 
174
180
  **Otros lenguajes:** en Python, C#, Java, Go y VB.NET, Parche detecta integraciones SOAP propias y clientes SOAP generados sin el campo (no los reescribe).
175
181
 
@@ -201,7 +207,7 @@ Cuando `DocTipo` no es un número fijo (`DocTipo: docTipo`), Parche no puede sab
201
207
  ## Límites del MVP
202
208
 
203
209
  - JS/TS (ESM y CommonJS) y, para ARCA, PHP. Siguen Python y Java.
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.
210
+ - ARCA: cubre `@afipsdk/afip.js`, `@arcasdk/core` (`arca.electronicBillingService.createVoucher` y `createNextVoucher`) y `afipsdk/afip.php` (con sus forks). Las integraciones SOAP propias (en cualquier lenguaje) se detectan pero no se reescriben, salvo el renombre de una clave casi bien escrita.
205
211
  - 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
212
  - 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.
207
213
  - 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.
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
 
@@ -15,6 +15,7 @@
15
15
  "afip.ts": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
16
16
  "ts-afip-ws": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
17
17
  "facturajs": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
18
+ "facturas": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
18
19
  "openai": { "provider": "OpenAI", "category": "ia" },
19
20
  "@anthropic-ai/sdk": { "provider": "Anthropic", "category": "ia" },
20
21
  "@google/generative-ai": { "provider": "Google (Gemini)", "category": "ia" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.6.8",
3
+ "version": "0.7.0",
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,24 +1,24 @@
1
1
  {
2
2
  "id": "arca.wsfe.condicion-iva-receptor",
3
- "version": "2026.10.16",
3
+ "version": "2026.10.24",
4
4
  "provider": "ARCA (ex AFIP)",
5
5
  "title": "Condición frente al IVA del receptor obligatoria en WSFEv1",
6
6
  "kind": "breaking",
7
7
  "severity": "critical",
8
8
  "deadline": "2026-12-01",
9
- "last_verified": "2026-10-16",
9
+ "last_verified": "2026-10-24",
10
10
  "summary_es": "Desde el 1/12/2026 ARCA rechaza automáticamente las solicitudes de CAE que no informen CondicionIVAReceptorId.",
11
- "why_es": "La RG 5616/2024 hizo obligatorio informar la condición frente al IVA del receptor en cada comprobante electrónico. El manual del desarrollador de WSFEv1 (v4.8) fija el 1 de diciembre de 2026 como fecha desde la cual el campo es obligatorio: si viaja vacío, nulo o no viaja, el pedido de CAE se rechaza y no se puede facturar.",
11
+ "why_es": "La RG 5616/2024 hizo obligatorio informar la condición frente al IVA del receptor en cada comprobante electrónico. El manual del desarrollador de WSFEv1 fija el 1 de diciembre de 2026 como fecha desde la cual el campo es obligatorio: si viaja vacío, nulo o no viaja, el pedido de CAE se rechaza y no se puede facturar.",
12
12
  "sources": [
13
- { "title": "VATupdate: Argentina makes recipient VAT status mandatory (WSFEv1 v4.8)", "url": "https://www.vatupdate.com/2026/09/09/argentina-makes-recipient-vat-status-mandatory-in-electronic-invoices/" },
13
+ { "title": "VATupdate: Argentina makes recipient VAT status mandatory in electronic invoices", "url": "https://www.vatupdate.com/2026/09/09/argentina-makes-recipient-vat-status-mandatory-in-electronic-invoices/" },
14
14
  { "title": "iProfesional: ARCA rechazará facturas sin la condición del cliente frente al IVA", "url": "https://www.iprofesional.com/impuestos/464104-nuevo-requisito-arca-puede-trabar-emision-facturas" },
15
15
  { "title": "Tabla de códigos de FEParamGetCondicionIvaReceptor", "url": "https://app.spomsolutions.com/blog/condicion-iva-receptor-rg-5616" },
16
16
  { "title": "Afip SDK: factura electrónica (Node y PHP, CreateVoucher / CreateNextVoucher)", "url": "https://docs.afipsdk.com/siguientes-pasos/web-services/factura-electronica" },
17
17
  { "title": "afipsdk/afip.php en Packagist", "url": "https://packagist.org/packages/afipsdk/afip.php" }
18
18
  ],
19
19
  "detect": {
20
- "packages": ["@afipsdk/afip.js", "@arcasdk/core", "afip.ts", "ts-afip-ws", "facturajs", "afipsdk/afip.php", "fertandil87/afip.php", "ivanalemunioz/afip.php", "eesnaola/afip.php"],
21
- "calls": ["ElectronicBilling.createVoucher", "ElectronicBilling.createNextVoucher", "ElectronicBilling->CreateVoucher", "ElectronicBilling->CreateNextVoucher"],
20
+ "packages": ["@afipsdk/afip.js", "@arcasdk/core", "afip.ts", "ts-afip-ws", "facturajs", "facturas", "afipsdk/afip.php", "fertandil87/afip.php", "ivanalemunioz/afip.php", "eesnaola/afip.php"],
21
+ "calls": ["ElectronicBilling.createVoucher", "ElectronicBilling.createNextVoucher", "ElectronicBilling->CreateVoucher", "ElectronicBilling->CreateNextVoucher", "electronicBillingService.createVoucher", "electronicBillingService.createNextVoucher"],
22
22
  "raw_markers": ["FECAESolicitar", "FECAEDetRequest"],
23
23
  "field": "CondicionIVAReceptorId"
24
24
  },
@@ -44,8 +44,10 @@
44
44
  "codemod": "arca-wsfe-condicion-iva",
45
45
  "review_checklist_es": [
46
46
  "Probar la emisión en homologación (wsfehomo) con cada tipo de comprobante que emitís.",
47
- "Confirmar que cada cliente con CUIT tiene guardada su condición frente al IVA y que se envía en cada CAE.",
48
47
  "Pedir la condición frente al IVA en el alta de clientes nuevos.",
49
48
  "Validar los códigos con FEParamGetCondicionIvaReceptor (la tabla puede cambiar)."
49
+ ],
50
+ "review_checklist_if_pending_es": [
51
+ "Confirmar que cada cliente con CUIT tiene guardada su condición frente al IVA y que se envía en cada CAE."
50
52
  ]
51
53
  }
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
+ }
package/src/ast.js CHANGED
@@ -23,11 +23,12 @@ export function eolOf(source) {
23
23
  return crlf > lf ? '\r\n' : '\n';
24
24
  }
25
25
 
26
- // Aplica inserciones de texto { offset, text } de atrás para adelante, así no se corren los offsets.
26
+ // Aplica ediciones de texto { offset, text, remove } de atrás para adelante, así no se corren los offsets.
27
+ // Sin `remove` es una inserción; con `remove`, `text` reemplaza esa cantidad de caracteres desde `offset`.
27
28
  export function applyEdits(source, edits) {
28
29
  let out = source;
29
30
  for (const e of [...edits].sort((a, b) => b.offset - a.offset)) {
30
- out = out.slice(0, e.offset) + e.text + out.slice(e.offset);
31
+ out = out.slice(0, e.offset) + e.text + out.slice(e.offset + (e.remove || 0));
31
32
  }
32
33
  return out;
33
34
  }
@@ -79,6 +79,25 @@ export function partialLeftMessage(pending) {
79
79
  return `Tiene el arreglo parcial de Parche (solo DocTipo 99 → 5). ${pending.message}`;
80
80
  }
81
81
 
82
+ // Clave casi igual al campo (frente B, punto 13): `CondicionIVAReceptor` sin Id, `CondicionIvaReceptorId`,
83
+ // `condicionIVAReceptorId`. Ese elemento no existe en el WSDL de WSFEv1, así que el dato nunca viaja y el repo
84
+ // parece migrado (pablocla/erpia lo tenía en cuatro archivos). Solo se toca dentro del detalle de un comprobante
85
+ // (dos o más claves de WSFEv1), para no renombrar modelos o DTOs propios que se llamen parecido.
86
+ const FIELD = 'CondicionIVAReceptorId';
87
+ export const NEAR_HINT = /condicion_?iva_?receptor/i;
88
+ const DETAIL_KEYS = new Set(['Concepto', 'DocTipo', 'DocNro', 'CbteDesde', 'CbteHasta', 'CbteFch', 'ImpTotal', 'ImpNeto', 'ImpIVA', 'MonId', 'MonCotiz']);
89
+ export const isNearMissKey = (key) => typeof key === 'string' && key !== FIELD && /^condicion_?iva_?receptor(?:_?id)?$/i.test(key);
90
+ export const isVoucherDetail = (keys) => keys.filter((k) => DETAIL_KEYS.has(k)).length >= 2;
91
+
92
+ export function nearMissMessage(key, pack) {
93
+ return `\`${key}\` no es un campo de WSFEv1: ARCA espera \`${FIELD}\`, así que hoy el dato no llega y desde el ${dmy(pack.deadline)} ARCA rechaza el CAE. `
94
+ + 'Lo renombré sin tocar el valor: revisá que sea un código de FEParamGetCondicionIvaReceptor.';
95
+ }
96
+
97
+ export function nearMissBothMessage(key) {
98
+ return `El comprobante tiene \`${key}\` y también \`${FIELD}\`. \`${key}\` no es un campo de WSFEv1: se puede borrar. Revisá que \`${FIELD}\` lleve el valor correcto.`;
99
+ }
100
+
82
101
  // "El repo ya usa CondicionIVAReceptorId en otro lado": prefijo para bajar a "revisar" lo que no se pudo leer
83
102
  // (en repos ya migrados, el objeto suele salir de un builder que sí pone el campo).
84
103
  export function alreadyMigratedPrefix(context) {
@@ -92,7 +111,7 @@ export function libraryCaveat(context, ecosystem) {
92
111
  const lib = (context?.arcaLibs || []).find((l) => l.ecosystem === ecosystem && l.travels !== 'si');
93
112
  if (lib) {
94
113
  return `Ojo: con ${lib.name} ${lib.version} el pedido se arma con un WSDL de 2021 que no tiene este campo`
95
- + `${lib.travels === 'no' ? '' : ' (en JS falta confirmarlo)'}: para que llegue a ARCA hay que actualizar la librería a 1.x.`;
114
+ + `${lib.travels === 'no' ? '' : ' (en JS falta confirmarlo)'}: para que llegue a ARCA hay que ${lib.fix || 'actualizar la librería a 1.x'}.`;
96
115
  }
97
116
  const wsdl = context?.staleWsdl?.[0];
98
117
  if (wsdl) return `Ojo: el repo tiene un WSDL de WSFEv1 sin este campo (\`${wsdl}\`): si el cliente SOAP lo usa, el campo no viaja hasta actualizarlo.`;
@@ -8,8 +8,10 @@ import { parsePhp, walkPhp, phpLiteral, phpKey } from '../php.js';
8
8
  import {
9
9
  PACK, decideCondition, validCodesText, missingMessage, appliedMessage, soapDetection, ARCA_HINT, libraryCaveat, alreadyMigratedPrefix,
10
10
  canApplyAtRuntime, PARTIAL_COMMENT, partialAppliedMessage, partialPending, partialLeftMessage,
11
+ NEAR_HINT, isNearMissKey, isVoucherDetail, nearMissMessage, nearMissBothMessage,
11
12
  } from './arca-rules.js';
12
13
  import { emptyContext } from '../context.js';
14
+ import { isTestFile } from '../paths.js';
13
15
 
14
16
  // En PHP los nombres de método no distinguen mayúsculas: createVoucher también es válido.
15
17
  // El inventario usa el nombre oficial, así un mismo método no aparece dos veces.
@@ -112,7 +114,7 @@ function resolveData(arg, scope, call, scopes) {
112
114
  export default function transform({ source, file, pack, context = emptyContext() }) {
113
115
  const findings = [];
114
116
  const usages = [];
115
- if (!ARCA_HINT.test(source)) return { output: null, findings, usages }; // prefiltro (ver el codemod de JS)
117
+ if (!ARCA_HINT.test(source) && !NEAR_HINT.test(source)) return { output: null, findings, usages }; // prefiltro (ver el codemod de JS)
116
118
  const caveat = libraryCaveat(context, 'composer');
117
119
  const migrated = alreadyMigratedPrefix(context);
118
120
  let ast;
@@ -129,6 +131,28 @@ export default function transform({ source, file, pack, context = emptyContext()
129
131
  const { scopes, calls } = collect(ast);
130
132
  const edits = [];
131
133
  const edited = new Set(); // el mismo $data en dos llamadas: se inserta una sola vez (ver el codemod de JS)
134
+
135
+ // Punto 13 del frente B: 'CondicionIVAReceptor' => $x (sin Id) se renombra, como en JS. Solo cambia el texto
136
+ // entre las comillas de la clave; el array queda marcado para que la llamada no reciba un campo nuevo.
137
+ let renamed = false;
138
+ if (!isTestFile(file)) {
139
+ walkPhp(ast, (node) => {
140
+ if (node.kind !== 'array') return;
141
+ const entries = node.items.filter((it) => it && it.kind === 'entry');
142
+ const near = entries.find((e) => isNearMissKey(phpKey(e)));
143
+ if (!near || !isVoucherDetail(entries.map(phpKey))) return;
144
+ edited.add(node);
145
+ if (entries.some((e) => phpKey(e) === FIELD)) {
146
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissBothMessage(phpKey(near))));
147
+ return;
148
+ }
149
+ const { start, end } = near.key.loc;
150
+ edits.push({ offset: start.offset + 1, remove: end.offset - start.offset - 2, text: FIELD });
151
+ renamed = true;
152
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissMessage(phpKey(near), pack) + (caveat ? ` ${caveat}` : '')));
153
+ });
154
+ }
155
+
132
156
  for (const { node: call, scope, method } of calls) {
133
157
  usages.push({ provider: 'arca', op: `ElectronicBilling->${method}`, line: call.loc.start.line });
134
158
  const data = resolveData(call.arguments[0], scope, call, scopes);
@@ -136,7 +160,7 @@ export default function transform({ source, file, pack, context = emptyContext()
136
160
  const entries = data.arr ? data.arr.items.filter((it) => it && it.kind === 'entry') : null;
137
161
  if (!entries || entries.some((e) => e.unpack || e.byRef)) {
138
162
  const msg = data.hint || `No pude leer los datos del comprobante (${data.reason || 'el array usa ...$otro o referencias'}). Agregá ${FIELD} a mano, ${HINT_TAIL}`;
139
- findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + msg));
163
+ findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + msg + (caveat ? ` ${caveat}` : '')));
140
164
  continue;
141
165
  }
142
166
  const byKey = (k) => entries.find((e) => phpKey(e) === k);
@@ -175,7 +199,7 @@ export default function transform({ source, file, pack, context = emptyContext()
175
199
  findings.push(finding(PACK, file, call, status, appliedMessage(d.status) + (caveat ? ` ${caveat}` : '')));
176
200
  }
177
201
 
178
- const soap = soapDetection(source, file, context);
202
+ const soap = renamed ? null : soapDetection(source, file, context); // ya manda el campo (ver el codemod de JS)
179
203
  if (soap) { findings.push(soap.finding); usages.push(soap.usage); }
180
204
 
181
205
  return { output: edits.length ? applyEdits(source, edits) : null, findings, usages };
@@ -2,13 +2,15 @@
2
2
  // Agrega CondicionIVAReceptorId a cada pedido de CAE hecho con afip.js cuando se puede inferir
3
3
  // sin datos del negocio, y marca el resto para que una persona complete el dato.
4
4
  import {
5
- jFor, detectQuote, eolOf, offsetAt, applyEdits, getProp, isQuotedKey, isPlainProp, literalValue, resolveBinding, finding,
5
+ jFor, detectQuote, eolOf, offsetAt, applyEdits, getProp, isQuotedKey, isPlainProp, keyName, literalValue, resolveBinding, finding,
6
6
  } from '../ast.js';
7
7
  import {
8
8
  PACK, decideCondition, validCodesText, missingMessage, appliedMessage, soapDetection, ARCA_HINT, libraryCaveat, alreadyMigratedPrefix,
9
9
  canApplyAtRuntime, PARTIAL_COMMENT, partialAppliedMessage, partialPending, partialLeftMessage,
10
+ NEAR_HINT, isNearMissKey, isVoucherDetail, nearMissMessage, nearMissBothMessage,
10
11
  } from './arca-rules.js';
11
12
  import { emptyContext } from '../context.js';
13
+ import { isTestFile } from '../paths.js';
12
14
 
13
15
  const VOUCHER_METHODS = new Set(['createVoucher', 'createNextVoucher']);
14
16
  const WRAPPERS = new Set(['TSAsExpression', 'TSNonNullExpression', 'TSTypeAssertion', 'TSSatisfiesExpression', 'ParenthesizedExpression']);
@@ -18,7 +20,10 @@ const unwrap = (n) => {
18
20
  while (n && WRAPPERS.has(n.type)) n = n.expression;
19
21
  return n;
20
22
  };
21
- const isEBMember = (n) => !!(n && n.type === 'MemberExpression' && !n.computed && n.property.type === 'Identifier' && n.property.name === 'ElectronicBilling');
23
+ // afip.ElectronicBilling (afip.js) o arca.electronicBillingService (@arcasdk/core): las dos reciben el mismo
24
+ // comprobante, con las claves de WSFEv1 (tipos de @arcasdk/core 2.0.2, leídos en su paquete de npm el 24/10/2026).
25
+ const BILLING_MEMBERS = new Set(['ElectronicBilling', 'electronicBillingService']);
26
+ const isEBMember = (n) => !!(n && n.type === 'MemberExpression' && !n.computed && n.property.type === 'Identifier' && BILLING_MEMBERS.has(n.property.name));
22
27
 
23
28
  // ¿`node` es `<algo>.ElectronicBilling`? También con casts de TS y con un alias en el mismo scope
24
29
  // (`const eb = afip.ElectronicBilling; eb.createVoucher(...)`): los dos aparecieron en repos reales.
@@ -32,6 +37,13 @@ function isElectronicBilling(node, p) {
32
37
  return false;
33
38
  }
34
39
 
40
+ // Para el inventario: `ElectronicBilling` o `electronicBillingService`, también si la llamada pasa por un alias.
41
+ function billingName(node, p) {
42
+ let n = unwrap(node);
43
+ if (n && n.type === 'Identifier') n = unwrap(resolveBinding(p, n.name)?.node.init);
44
+ return isEBMember(n) ? n.property.name : 'ElectronicBilling';
45
+ }
46
+
35
47
  function voucherCallee(p) {
36
48
  const callee = unwrap(p.node.callee);
37
49
  if (!callee || callee.type !== 'MemberExpression' || callee.computed || callee.property.type !== 'Identifier') return null;
@@ -119,7 +131,8 @@ const suggestionFor = (d, pack) => `'CondicionIVAReceptorId': <condición del cl
119
131
  export default function transform({ source, file, pack, context = emptyContext() }) {
120
132
  const findings = [];
121
133
  const usages = [];
122
- if (!ARCA_HINT.test(source)) return { output: null, findings, usages };
134
+ const arcaFile = ARCA_HINT.test(source);
135
+ if (!arcaFile && !NEAR_HINT.test(source)) return { output: null, findings, usages };
123
136
  const caveat = libraryCaveat(context, 'npm');
124
137
  const migrated = alreadyMigratedPrefix(context);
125
138
  const j = jFor(file);
@@ -128,8 +141,10 @@ export default function transform({ source, file, pack, context = emptyContext()
128
141
  root = j(source);
129
142
  } catch (err) {
130
143
  // Antes se descartaba en silencio: un archivo de ARCA que no parsea es justo el que hay que mirar.
131
- findings.push({ pack: PACK, file, line: err.loc?.line || null, status: 'needs-input',
132
- message: `No pude analizar este archivo (${err.message.split('\n')[0]}). Revisá a mano las llamadas a ElectronicBilling.` });
144
+ if (arcaFile) {
145
+ findings.push({ pack: PACK, file, line: err.loc?.line || null, status: 'needs-input',
146
+ message: `No pude analizar este archivo (${err.message.split('\n')[0]}). Revisá a mano las llamadas a ElectronicBilling.` });
147
+ }
133
148
  return { output: null, findings, usages };
134
149
  }
135
150
  let changed = false;
@@ -138,9 +153,31 @@ export default function transform({ source, file, pack, context = emptyContext()
138
153
  // texto y el árbol no se entera, sin esto se insertaba dos veces → clave duplicada y TypeScript no compila.
139
154
  const edited = new Set();
140
155
 
156
+ // Punto 13 del frente B: la clave casi bien escrita se renombra; el nombre correcto lo fija el WSDL.
157
+ // Va antes que las llamadas, así el comprobante renombrado no recibe además un CondicionIVAReceptorId nuevo.
158
+ let renamed = false;
159
+ if (!isTestFile(file)) {
160
+ root.find(j.ObjectExpression).forEach((p) => {
161
+ const obj = p.node;
162
+ const props = obj.properties.filter(isPlainProp);
163
+ const near = props.find((pr) => isNearMissKey(keyName(pr)));
164
+ if (!near || !isVoucherDetail(props.map(keyName))) return;
165
+ edited.add(obj);
166
+ if (getProp(obj, 'CondicionIVAReceptorId')) {
167
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissBothMessage(keyName(near))));
168
+ return;
169
+ }
170
+ edits.push(renameKey(source, near));
171
+ renamed = true;
172
+ changed = true;
173
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissMessage(keyName(near), pack) + (caveat ? ` ${caveat}` : '')));
174
+ });
175
+ }
176
+
141
177
  root.find(j.CallExpression).filter((p) => voucherCallee(p) !== null).forEach((p) => {
142
178
  const call = p.node;
143
- usages.push({ provider: 'arca', op: `ElectronicBilling.${voucherCallee(p).property.name}`, line: call.loc?.start.line });
179
+ const callee = voucherCallee(p);
180
+ usages.push({ provider: 'arca', op: `${billingName(callee.object, p)}.${callee.property.name}`, line: call.loc?.start.line });
144
181
  const arg = call.arguments[0];
145
182
  let obj = null;
146
183
  let binding = null;
@@ -151,7 +188,9 @@ export default function transform({ source, file, pack, context = emptyContext()
151
188
  }
152
189
  if (!obj || otherSpread(obj)) {
153
190
  // Si el repo ya usa el campo en otro lado, lo más probable es que el builder ya lo ponga: "revisar".
154
- findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + unreadableMessage(j, root, p, arg, obj)));
191
+ // El aviso de librería vieja también va acá (frente B, punto 14: en los repos de jretondo los datos llegan de afuera).
192
+ findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input',
193
+ (migrated || '') + unreadableMessage(j, root, p, arg, obj) + (caveat ? ` ${caveat}` : '')));
155
194
  return;
156
195
  }
157
196
  if (getProp(obj, 'CondicionIVAReceptorId') || edited.has(obj)) return; // ya está migrado (o ya lo editamos)
@@ -196,12 +235,23 @@ export default function transform({ source, file, pack, context = emptyContext()
196
235
  findings.push(finding(PACK, file, call, status, appliedMessage(d.status) + (caveat ? ` ${caveat}` : '')));
197
236
  });
198
237
 
199
- const soap = soapDetection(source, file, context);
238
+ // Con la clave renombrada el archivo ya manda el campo: el aviso de SOAP sin CondicionIVAReceptorId sobra.
239
+ const soap = renamed ? null : soapDetection(source, file, context);
200
240
  if (soap) { findings.push(soap.finding); usages.push(soap.usage); }
201
241
 
202
242
  return { output: changed ? applyEdits(source, edits) : null, findings, usages };
203
243
  }
204
244
 
245
+ // `CondicionIVAReceptor: x` → `CondicionIVAReceptorId: x`, con las mismas comillas y sin tocar el valor.
246
+ // Si era abreviada (`{ CondicionIVAReceptor }`), queda `CondicionIVAReceptorId: CondicionIVAReceptor`.
247
+ function renameKey(source, prop) {
248
+ const start = offsetAt(source, prop.key.loc.start);
249
+ const end = offsetAt(source, prop.key.loc.end);
250
+ if (prop.shorthand) return { offset: start, remove: end - start, text: `CondicionIVAReceptorId: ${keyName(prop)}` };
251
+ const q = isQuotedKey(prop) ? source[start] : '';
252
+ return { offset: start, remove: end - start, text: `${q}CondicionIVAReceptorId${q}` };
253
+ }
254
+
205
255
  // Cuando no se puede leer el comprobante, decir DÓNDE hay que agregar el dato: la función que lo arma
206
256
  // (y de qué módulo viene) o que llega como parámetro. Lo encontrado en repos reales: payload = buildVoucherPayload(...).
207
257
  function unreadableMessage(j, root, p, arg, obj) {
package/src/context.js CHANGED
@@ -59,8 +59,13 @@ export const openRange = (range) => /^\s*(?:>=?|\*|x|latest)/i.test(range || '')
59
59
  // afip.js / afip.php ≥ 1.0: mandan JSON a la API de Afip SDK → sí.
60
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).
61
61
  // fertandil87/afip.php: actualizó su WSDL (lo incluye) → sí. eesnaola/afip.php: WSDL de 2021 → igual que 0.x.
62
+ // ts-afip-ws: fork de afip.js 0.x (frente B, punto 14: los repos de jretondo). Su última versión, la 1.3.2 de 2023,
63
+ // trae wsfe.wsdl y wsfe-production.wsdl sin el campo (leído en el paquete de npm el 24/10/2026) → sin confirmar.
62
64
  const ARCA_LIBS = {
63
- npm: { '@afipsdk/afip.js': (v) => (major(v) >= 1 ? 'si' : 'sin-confirmar') },
65
+ npm: {
66
+ '@afipsdk/afip.js': (v) => (major(v) >= 1 ? 'si' : 'sin-confirmar'),
67
+ 'ts-afip-ws': () => 'sin-confirmar',
68
+ },
64
69
  composer: {
65
70
  'afipsdk/afip.php': (v) => (major(v) >= 1 ? 'si' : 'no'),
66
71
  'ivanalemunioz/afip.php': (v) => (major(v) >= 1 ? 'si' : 'no'),
@@ -69,13 +74,18 @@ const ARCA_LIBS = {
69
74
  },
70
75
  };
71
76
  const major = (v) => Number(String(v).split('.')[0]);
77
+ // Qué hacer para que el campo viaje, cuando no es "actualizar la librería a 1.x" (los forks no tienen 1.x).
78
+ const FIXES = {
79
+ 'ts-afip-ws': 'pasar a @afipsdk/afip.js 1.x o actualizar el WSDL que trae la librería',
80
+ 'eesnaola/afip.php': 'pasar a afipsdk/afip.php 1.x o actualizar el WSDL que trae la librería',
81
+ };
72
82
 
73
83
  export function arcaLibraries(versions) {
74
84
  const out = [];
75
85
  for (const [eco, libs] of Object.entries(ARCA_LIBS)) {
76
86
  for (const [name, rule] of Object.entries(libs)) {
77
87
  const v = versions[eco]?.[name];
78
- if (v) out.push({ ecosystem: eco, name, version: v.version, source: v.source, travels: rule(v.version) });
88
+ if (v) out.push({ ecosystem: eco, name, version: v.version, source: v.source, travels: rule(v.version), ...(FIXES[name] ? { fix: FIXES[name] } : {}) });
79
89
  }
80
90
  }
81
91
  return out;
@@ -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('');
@@ -158,9 +158,15 @@ export function prBody({ pack, findings, changed, verification, ciApproval = fal
158
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
159
  }
160
160
  }
161
- out.push('', asIssue ? '### Checklist' : '### Checklist antes de mergear', '', ...pack.review_checklist_es.map((x) => `- [ ] ${x}`), '');
161
+ // Los ítems que dependen de un dato del negocio (por ejemplo, los clientes con CUIT) solo van si queda algo
162
+ // pendiente: en un PR que arregla todo, como uno de puros consumidores finales, no aplican (frente B, punto 12).
163
+ const checklist = [...pack.review_checklist_es, ...(pending.length ? pack.review_checklist_if_pending_es || [] : [])];
164
+ out.push('', asIssue ? '### Checklist' : '### Checklist antes de mergear', '', ...checklist.map((x) => `- [ ] ${x}`), '');
162
165
  out.push('### Fuentes', '', ...pack.sources.map((s) => `- [${s.title}](${s.url})`), '');
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>`, '');
166
+ const feedback = asIssue
167
+ ? '¿Te sirvió este aviso? Cerralo cuando lo resuelvas, o contestá si algo no aplica: Parche aprende de cada resultado.'
168
+ : '¿Este PR te sirvió? Mergealo, editalo o cerralo: Parche aprende de cada resultado.';
169
+ 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(' · ')}. ${feedback} [¿Qué es Parche y por qué recibiste esto?](https://github.com/Pepo08/parche/blob/main/docs/por-que-recibi-este-pr.md)</sub>`, '');
164
170
  return out.join('\n');
165
171
  }
166
172
 
@@ -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