parche-cli 0.6.0 → 0.6.2
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 +21 -0
- package/README.md +4 -2
- package/bin/parche.js +26 -8
- package/package.json +1 -1
- package/packs/ai.model-retirements.json +16 -3
- package/src/codemods/arca-wsfe-condicion-iva.js +29 -1
- package/src/detectors/ai-model-retirements.js +18 -0
- package/src/engine.js +14 -6
- package/src/git.js +15 -1
- package/src/paths.js +22 -5
- package/src/report.js +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
Todas las versiones de `parche-cli` (npm) y de la Action `Pepo08/parche@v0`.
|
|
4
4
|
|
|
5
|
+
## 0.6.2 — 2026-10-18
|
|
6
|
+
|
|
7
|
+
Pack de IA completo para el apagado de OpenAI del **23/10/2026** (sección "2026-04-22: Legacy GPT model snapshots" de la página oficial, verificada el 18/10).
|
|
8
|
+
|
|
9
|
+
### Nuevo
|
|
10
|
+
- **Alias `-completions`:** `gpt-3.5-turbo-completions` (reemplazo `gpt-5.6-terra`), `gpt-4-completions`, `gpt-4-0613-completions` y `gpt-4-turbo-completions` (reemplazo `gpt-5.6-sol`).
|
|
11
|
+
- **Modelos fine-tuned:** se avisan los IDs reales (`ft:<modelo base>:<org>:<sufijo>:<id>`, también con `:ckpt-step-…`) de las familias que OpenAI apaga: `ft-gpt-3.5-turbo` y `ft-babbage-002`, `ft-davinci-002` y `ft-o4-mini-2025-04-16` (base de reemplazo `gpt-5.6-terra`), `ft-gpt-4` (`gpt-5.6-sol`) y `ft-gpt-4.1-nano-2025-04-14` (`gpt-5.6-luna`). Se compara el modelo base entero: `ft:gpt-4o-mini-…`, `ft:gpt-4o-…` y `ft:gpt-4.1-…` no son de `gpt-4` y no se avisan (OpenAI los apaga en otra fecha). Como siempre, Parche avisa y no cambia el modelo.
|
|
12
|
+
- El pack cita la guía de fine-tuning de OpenAI como fuente del formato de los IDs. `last_verified`: 2026-10-18 (Anthropic y el resto de OpenAI, revisados contra sus páginas ese día: sin cambios).
|
|
13
|
+
|
|
14
|
+
## 0.6.1 — 2026-10-18
|
|
15
|
+
|
|
16
|
+
Lo que encontró el frente de tracción al preparar los PRs con la 0.6.0.
|
|
17
|
+
|
|
18
|
+
### Arreglado
|
|
19
|
+
- **ARCA en JS: si el objeto recibe el campo después, cuenta como migrado**, igual que en PHP: `voucher.CondicionIVAReceptorId = …`, `voucher['CondicionIVAReceptorId'] = …`, con cast de TS u `Object.assign(voucher, { CondicionIVAReceptorId: … })`. Antes se agregaba el arreglo parcial igual, y era redundante. Una variable con el mismo nombre en otra función no cuenta.
|
|
20
|
+
- **Copias de respaldo:** no se leen ni se editan las carpetas `BAK/`, `backup(s)/`, `old/`, `_old/` y `deprecated/`, ni los archivos `* copy.*`, `* - Copy.*`, `* copia.*`, `*.bak.*`, `*-old.*`, `*_old.*` y `*.old.*`. Tampoco cuentan sus manifiestos ni sus WSDL. El scan y `report.md` dicen qué se saltó.
|
|
21
|
+
- **Windows:** los comandos de git corren con `core.longpaths`. Sin eso, un clon recién hecho con rutas largas daba "Hay cambios sin commitear".
|
|
22
|
+
- **`parche pr --author "Nombre <email>"`** firma con esa identidad sin tocar la configuración de git. `pr` muestra quién firma y, antes de pushear, avisa si el email no es un noreply de GitHub (queda público en el PR).
|
|
23
|
+
- Si solo cambiarían tests o mocks, el texto queda como issue: ya no empieza con "Modifica 1 archivo".
|
|
24
|
+
- La consola ya no repite "No cubre: No cubre…" en los arreglos parciales.
|
|
25
|
+
|
|
5
26
|
## 0.6.0 — 2026-10-17
|
|
6
27
|
|
|
7
28
|
### Nuevo
|
package/README.md
CHANGED
|
@@ -131,13 +131,15 @@ Con el tag, el workflow corre los tests, controla que la versión de `package.js
|
|
|
131
131
|
|---|---|---|---|---|
|
|
132
132
|
| `arca.wsfe.condicion-iva-receptor` | ARCA | rompe | 2026-12-01 | sí, en JS (`afip.js`) y PHP (`afipsdk/afip.php` y forks): consumidor final y DNI B/C; con `DocTipo` en una variable, consumidor final al ejecutarse (PR parcial) |
|
|
133
133
|
| `mercadopago.payments-to-orders` | Mercado Pago | migración | — | sí (creación de pagos en SDK Node v2) |
|
|
134
|
-
| `ai.model-retirements` | Anthropic, OpenAI | retiro | varias | solo detección |
|
|
134
|
+
| `ai.model-retirements` | Anthropic, OpenAI | retiro | varias | solo detección (también modelos fine-tuned de OpenAI, `ft:…`) |
|
|
135
135
|
|
|
136
136
|
## Verificación
|
|
137
137
|
|
|
138
138
|
Antes de commitear, Parche parsea **siempre** cada archivo modificado (JS/TS o PHP): eso no ejecuta nada del repo. Con `--verify`, además corre los tests del repo que correspondan a lo que cambió: `npm test` para JS/TS; para PHP, `composer test` o `vendor/bin/phpunit`. Si no hay tests, queda el chequeo de sintaxis y el PR lo dice. Sin `--verify` (por ejemplo, en un PR a un repo de terceros, donde no se ejecuta su código), el PR dice que Parche no corrió los tests y nombra los workflows del repo que los corren en cada pull request. Si algo falla, no hay PR. El commit lleva la versión del pack que lo generó, para poder rastrear qué regla produjo cada cambio.
|
|
139
139
|
|
|
140
|
-
**Quién firma el commit:** `PARCHE_GIT_NAME` y `PARCHE_GIT_EMAIL` si están; si no, tu identidad de git (`user.name` y `user.email`, también la que configure tu workflow); en GitHub Actions sin identidad, `github-actions[bot]`, el mismo que abre el PR. Si no hay ninguna, `parche pr` frena antes de tocar el repo.
|
|
140
|
+
**Quién firma el commit:** `--author "Nombre <email>"` si lo pasás; si no, `PARCHE_GIT_NAME` y `PARCHE_GIT_EMAIL` si están; si no, tu identidad de git (`user.name` y `user.email`, también la que configure tu workflow); en GitHub Actions sin identidad, `github-actions[bot]`, el mismo que abre el PR. Si no hay ninguna, `parche pr` frena antes de tocar el repo. Antes de pushear, si el email no es un noreply de GitHub, avisa: va a quedar público en el commit del PR.
|
|
141
|
+
|
|
142
|
+
**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.
|
|
141
143
|
|
|
142
144
|
## ARCA en PHP
|
|
143
145
|
|
package/bin/parche.js
CHANGED
|
@@ -22,9 +22,10 @@ Uso:
|
|
|
22
22
|
parche diff <paquete> --from <v> --to <v> [--json] [--all]
|
|
23
23
|
Qué cambia en la API de un paquete de npm entre dos versiones
|
|
24
24
|
parche fix <dir> [--pack <id>] Aplica los arreglos en el código
|
|
25
|
-
parche pr <dir> [--pack <id>] [--dry-run] [--verify] [--push-remote <remote>]
|
|
25
|
+
parche pr <dir> [--pack <id>] [--dry-run] [--verify] [--push-remote <remote>] [--author "Nombre <email>"]
|
|
26
26
|
Rama + commit + PR por cada cambio. Con --push-remote,
|
|
27
|
-
pushea a ese remote (tu fork) y abre el PR contra origin
|
|
27
|
+
pushea a ese remote (tu fork) y abre el PR contra origin.
|
|
28
|
+
Con --author, firma con esa identidad y no con la de git
|
|
28
29
|
parche feedback <dir> --pack <id> --outcome merged|edited|rejected [--note "..."]
|
|
29
30
|
`;
|
|
30
31
|
|
|
@@ -115,6 +116,13 @@ function printDiff(d, { all = false } = {}) {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// Lo que no se revisó por ser copia de respaldo: se dice, no se oculta.
|
|
120
|
+
function printSkipped(skipped = []) {
|
|
121
|
+
if (!skipped.length) return;
|
|
122
|
+
const shown = skipped.slice(0, 8).join(', ');
|
|
123
|
+
console.log(`\nNo revisé ${skipped.length} copia(s) de respaldo: ${shown}${skipped.length > 8 ? ` y ${skipped.length - 8} más` : ''}. Si alguna es código que corre, renombrala o movela.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
function printFindings(result) {
|
|
119
127
|
for (const pack of result.packs) {
|
|
120
128
|
const fs2 = result.findings.filter((f) => f.pack === pack.id);
|
|
@@ -123,7 +131,8 @@ function printFindings(result) {
|
|
|
123
131
|
console.log(`\n■ ${pack.title}\n ${pack.provider} · severidad ${pack.severity}${d}`);
|
|
124
132
|
for (const f of fs2) {
|
|
125
133
|
console.log(` ${LABEL[f.status].padEnd(16)} ${f.file}${f.line ? ':' + f.line : ''} ${f.message}`);
|
|
126
|
-
|
|
134
|
+
// partial.message ya empieza con "No cubre…": sin prefijo (antes salía "No cubre: No cubre").
|
|
135
|
+
if (f.partial) console.log(` ${''.padEnd(16)} ${f.partial.message}`);
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
}
|
|
@@ -171,11 +180,12 @@ async function main() {
|
|
|
171
180
|
fs.writeFileSync(path.join(root, '.parche', 'inventory.json'), JSON.stringify(result.inventory, null, 2));
|
|
172
181
|
fs.writeFileSync(path.join(root, '.parche', 'report.md'), scanReport({ ...result, changed: [], upgrades }));
|
|
173
182
|
record(root, { type: 'scan', findings: counts(result.findings) });
|
|
174
|
-
if (args.json) { console.log(JSON.stringify({ inventory: result.inventory, findings: result.findings }, null, 2)); return 0; }
|
|
183
|
+
if (args.json) { console.log(JSON.stringify({ inventory: result.inventory, findings: result.findings, skipped: result.skipped }, null, 2)); return 0; }
|
|
175
184
|
console.log(`Parche · scan de ${path.basename(root)}\n`);
|
|
176
185
|
printInventory(result.inventory);
|
|
177
186
|
if (upgrades) printUpgrades(upgrades);
|
|
178
187
|
printFindings(result);
|
|
188
|
+
printSkipped(result.skipped);
|
|
179
189
|
const s = summary(result.findings);
|
|
180
190
|
console.log(s ? `\n${s}` : '\nNinguno de los packs verificados tiene cambios que te afecten hoy.');
|
|
181
191
|
console.log(`Reporte: ${path.join(root, '.parche', 'report.md')}`);
|
|
@@ -197,7 +207,13 @@ async function main() {
|
|
|
197
207
|
if (!git.isRepo(root)) throw new Error('`parche pr` necesita un repositorio git.');
|
|
198
208
|
if (!git.isClean(root)) throw new Error('Hay cambios sin commitear. Commiteá o guardá (stash) antes de correr `parche pr`.');
|
|
199
209
|
// Quién firma el commit se resuelve antes de escribir nada: si falla al commitear, el repo quedaría sucio.
|
|
200
|
-
git.
|
|
210
|
+
const env = args.author !== undefined ? { ...process.env, ...git.parseAuthor(args.author) } : process.env;
|
|
211
|
+
const identity = git.commitIdentity(root, env);
|
|
212
|
+
console.log(`Firma los commits: ${identity.name} <${identity.email}>`);
|
|
213
|
+
// El email queda público en el PR: si no es un noreply de GitHub, se avisa antes de pushear (frente B, punto 20).
|
|
214
|
+
if (!args['dry-run'] && process.env.GITHUB_TOKEN && process.env.GITHUB_ACTIONS !== 'true' && !git.isNoreply(identity.email)) {
|
|
215
|
+
console.warn(`Ojo: ${identity.email} va a quedar público en el commit del PR. Si preferís no mostrarlo, usá --author "Nombre <usuario@users.noreply.github.com>".`);
|
|
216
|
+
}
|
|
201
217
|
ensureLocalDir(root);
|
|
202
218
|
const base = git.currentBranch(root);
|
|
203
219
|
const packs = loadPacks().filter((p) => p.codemod && (!packIds || packIds.includes(p.id)));
|
|
@@ -225,8 +241,10 @@ async function main() {
|
|
|
225
241
|
git.discardChanges(root);
|
|
226
242
|
throw new Error(`La verificación falló para ${pack.id}; no se abre el PR y el repo queda como estaba.\n${JSON.stringify(verification, null, 2)}`);
|
|
227
243
|
}
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
// Si solo cambiarían tests o mocks no hay PR: el texto queda como issue, sin "Modifica 1 archivo" (frente B, punto 20).
|
|
245
|
+
const reportOnly = !changedFiles.length || changedFiles.every(isTestFile);
|
|
246
|
+
fs.writeFileSync(bodyPath, prBody({ pack, findings: result.findings, changed: reportOnly ? [] : result.changed, verification: reportOnly ? null : verification }));
|
|
247
|
+
if (reportOnly) {
|
|
230
248
|
const why = changedFiles.length ? 'solo cambiaría tests o mocks: no se abre un PR' : 'sin cambios de código';
|
|
231
249
|
git.discardChanges(root);
|
|
232
250
|
console.log(` ${summary(result.findings)}`);
|
|
@@ -235,7 +253,7 @@ async function main() {
|
|
|
235
253
|
continue;
|
|
236
254
|
}
|
|
237
255
|
const draft = !!(c['needs-review'] || c['needs-input'] || c.blocked);
|
|
238
|
-
const sha = git.commitAll(root, `${title}\n\n${pack.summary_es}\nPack ${pack.id} v${pack.version}
|
|
256
|
+
const sha = git.commitAll(root, `${title}\n\n${pack.summary_es}\nPack ${pack.id} v${pack.version}.`, env);
|
|
239
257
|
keepBranch = true;
|
|
240
258
|
console.log(` Rama ${branch} · commit ${sha} · ${changedFiles.length} archivo(s)${verification ? ` · verificación ${verification.ok ? 'OK' : 'FALLÓ'}` : ''}`);
|
|
241
259
|
console.log(` ${summary(result.findings)}`);
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "ai.model-retirements",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.18",
|
|
4
4
|
"provider": "Modelos de IA",
|
|
5
5
|
"title": "Modelos y endpoints de IA retirados o por retirarse",
|
|
6
6
|
"kind": "retirement",
|
|
7
7
|
"severity": "high",
|
|
8
8
|
"deadline": null,
|
|
9
|
-
"last_verified": "2026-10-
|
|
9
|
+
"last_verified": "2026-10-18",
|
|
10
10
|
"summary_es": "Tu código usa modelos o APIs de IA que ya no responden o tienen fecha de retiro.",
|
|
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.",
|
|
12
12
|
"sources": [
|
|
13
13
|
{ "title": "Anthropic: Model deprecations", "url": "https://platform.claude.com/docs/en/about-claude/model-deprecations" },
|
|
14
|
-
{ "title": "OpenAI: Deprecations", "url": "https://developers.openai.com/api/docs/deprecations" }
|
|
14
|
+
{ "title": "OpenAI: Deprecations", "url": "https://developers.openai.com/api/docs/deprecations" },
|
|
15
|
+
{ "title": "OpenAI: Supervised fine-tuning (formato de los IDs ft:)", "url": "https://developers.openai.com/api/docs/guides/supervised-fine-tuning" }
|
|
15
16
|
],
|
|
16
17
|
"detect": {
|
|
17
18
|
"packages": ["@anthropic-ai/sdk", "openai"],
|
|
@@ -45,11 +46,15 @@
|
|
|
45
46
|
"o3-pro-2025-06-10": { "date": "2026-12-11", "replacement": "gpt-5.6-sol (reasoning.mode: pro)" },
|
|
46
47
|
"gpt-3.5-turbo": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
|
|
47
48
|
"gpt-3.5-turbo-0125": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
|
|
49
|
+
"gpt-3.5-turbo-completions": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
|
|
48
50
|
"gpt-4": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
49
51
|
"gpt-4-0613": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
52
|
+
"gpt-4-completions": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
53
|
+
"gpt-4-0613-completions": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
50
54
|
"gpt-4-1106-preview": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
51
55
|
"gpt-4-turbo": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
52
56
|
"gpt-4-turbo-2024-04-09": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
57
|
+
"gpt-4-turbo-completions": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
53
58
|
"gpt-4.1-nano": { "date": "2026-10-23", "replacement": "gpt-5.6-luna" },
|
|
54
59
|
"gpt-4.1-nano-2025-04-14": { "date": "2026-10-23", "replacement": "gpt-5.6-luna" },
|
|
55
60
|
"gpt-4o-2024-05-13": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
|
|
@@ -77,6 +82,14 @@
|
|
|
77
82
|
"chatgpt-4o-latest": { "date": "2026-02-17", "replacement": "gpt-5.1-chat-latest" },
|
|
78
83
|
"codex-mini-latest": { "date": "2026-02-12", "replacement": "gpt-5-codex-mini" }
|
|
79
84
|
},
|
|
85
|
+
"retired_fine_tunes": {
|
|
86
|
+
"ft-gpt-3.5-turbo": { "date": "2026-10-23", "replacement": "gpt-5.6-terra", "bases": ["gpt-3.5-turbo", "gpt-3.5-turbo-0125", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0613"] },
|
|
87
|
+
"ft-gpt-4": { "date": "2026-10-23", "replacement": "gpt-5.6-sol", "bases": ["gpt-4", "gpt-4-0613"] },
|
|
88
|
+
"ft-gpt-4.1-nano-2025-04-14": { "date": "2026-10-23", "replacement": "gpt-5.6-luna", "bases": ["gpt-4.1-nano-2025-04-14", "gpt-4.1-nano"] },
|
|
89
|
+
"ft-o4-mini-2025-04-16": { "date": "2026-10-23", "replacement": "gpt-5.6-terra", "bases": ["o4-mini-2025-04-16", "o4-mini"] },
|
|
90
|
+
"ft-babbage-002": { "date": "2026-10-23", "replacement": "gpt-5.6-terra", "bases": ["babbage-002"] },
|
|
91
|
+
"ft-davinci-002": { "date": "2026-10-23", "replacement": "gpt-5.6-terra", "bases": ["davinci-002"] }
|
|
92
|
+
},
|
|
80
93
|
"retirement_floors": {
|
|
81
94
|
"claude-haiku-4-5-20251001": "2026-10-15",
|
|
82
95
|
"claude-opus-4-5-20251101": "2026-11-24"
|
|
@@ -74,6 +74,32 @@ const otherSpread = (obj) => obj.properties.some((p) => (p.type === 'SpreadEleme
|
|
|
74
74
|
|
|
75
75
|
const textOf = (source, node) => source.slice(offsetAt(source, node.loc.start), offsetAt(source, node.loc.end));
|
|
76
76
|
|
|
77
|
+
// Nombre de la propiedad de un acceso: voucher.Campo o voucher['Campo'].
|
|
78
|
+
const memberName = (m) => (!m.computed && m.property.type === 'Identifier' ? m.property.name
|
|
79
|
+
: m.computed && (m.property.type === 'StringLiteral' || m.property.type === 'Literal') ? m.property.value : null);
|
|
80
|
+
|
|
81
|
+
// ¿El objeto de la variable `name` (declarada en `decl`) recibe el campo después de armarse?
|
|
82
|
+
// voucher.CondicionIVAReceptorId = …, voucher['CondicionIVAReceptorId'] = …, (voucher as any).CondicionIVAReceptorId = …
|
|
83
|
+
// u Object.assign(voucher, { CondicionIVAReceptorId: … }). Cuenta como migrado, igual que en PHP
|
|
84
|
+
// (frente B, punto 18: tometh04/vibook-services lo asigna en un if y Parche agregaba el arreglo parcial igual).
|
|
85
|
+
// Solo cuenta si es la misma variable: otra con el mismo nombre en otra función no.
|
|
86
|
+
function fieldAssignedLater(j, root, decl, name) {
|
|
87
|
+
const same = (p) => resolveBinding(p, name)?.node === decl.node;
|
|
88
|
+
const isVar = (n) => { const x = unwrap(n); return !!(x && x.type === 'Identifier' && x.name === name); };
|
|
89
|
+
const assigned = root.find(j.AssignmentExpression).filter((p) => {
|
|
90
|
+
const left = unwrap(p.node.left);
|
|
91
|
+
return left && left.type === 'MemberExpression' && isVar(left.object) && memberName(left) === 'CondicionIVAReceptorId' && same(p);
|
|
92
|
+
}).size() > 0;
|
|
93
|
+
if (assigned) return true;
|
|
94
|
+
return root.find(j.CallExpression).filter((p) => {
|
|
95
|
+
const c = unwrap(p.node.callee);
|
|
96
|
+
const isAssign = c && c.type === 'MemberExpression' && !c.computed && c.object.type === 'Identifier' && c.object.name === 'Object'
|
|
97
|
+
&& c.property.type === 'Identifier' && c.property.name === 'assign';
|
|
98
|
+
if (!isAssign || !isVar(p.node.arguments[0]) || !same(p)) return false;
|
|
99
|
+
return p.node.arguments.slice(1).some((a) => { const o = unwrap(a); return o && o.type === 'ObjectExpression' && !!getProp(o, 'CondicionIVAReceptorId'); });
|
|
100
|
+
}).size() > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
// La clave nueva copia el estilo del objeto: con comillas si la mayoría de las claves las tiene, y las
|
|
78
104
|
// mismas comillas que esas claves. Las del resto del archivo solo desempatan: en un repo real las claves
|
|
79
105
|
// iban con ' y el archivo usaba " en el resto, y la clave nueva salía distinta a sus vecinas.
|
|
@@ -117,10 +143,11 @@ export default function transform({ source, file, pack, context = emptyContext()
|
|
|
117
143
|
usages.push({ provider: 'arca', op: `ElectronicBilling.${voucherCallee(p).property.name}`, line: call.loc?.start.line });
|
|
118
144
|
const arg = call.arguments[0];
|
|
119
145
|
let obj = null;
|
|
146
|
+
let binding = null;
|
|
120
147
|
if (arg && arg.type === 'ObjectExpression') obj = arg;
|
|
121
148
|
else if (arg && arg.type === 'Identifier') {
|
|
122
149
|
const decl = resolveBinding(p, arg.name);
|
|
123
|
-
if (decl && decl.node.init && decl.node.init.type === 'ObjectExpression') obj = decl.node.init;
|
|
150
|
+
if (decl && decl.node.init && decl.node.init.type === 'ObjectExpression') { obj = decl.node.init; binding = decl; }
|
|
124
151
|
}
|
|
125
152
|
if (!obj || otherSpread(obj)) {
|
|
126
153
|
// Si el repo ya usa el campo en otro lado, lo más probable es que el builder ya lo ponga: "revisar".
|
|
@@ -128,6 +155,7 @@ export default function transform({ source, file, pack, context = emptyContext()
|
|
|
128
155
|
return;
|
|
129
156
|
}
|
|
130
157
|
if (getProp(obj, 'CondicionIVAReceptorId') || edited.has(obj)) return; // ya está migrado (o ya lo editamos)
|
|
158
|
+
if (binding && fieldAssignedLater(j, root, binding, arg.name)) return; // lo completa después: también migrado
|
|
131
159
|
const spreads = obj.properties.map(conditionSpread);
|
|
132
160
|
if (spreads.includes('full')) return;
|
|
133
161
|
|
|
@@ -8,6 +8,10 @@ const PACK = 'ai.model-retirements';
|
|
|
8
8
|
|
|
9
9
|
const MODEL_CONTEXT = /\bmodel(?:Id|Name)?\b\s*[:=(]|["']model["']\s*[:=]|\b(?:DEFAULT|default)_?(?:MODEL|Model)\b|\bmodel\s*\(/i;
|
|
10
10
|
const AZURE = /azure|AzureOpenAI|openai\.azure\.com/i;
|
|
11
|
+
// Modelos fine-tuned de OpenAI: el ID real es `ft:<modelo base>:<org>:<sufijo>:<id>` (guía de fine-tuning).
|
|
12
|
+
// Se compara el modelo base entero, hasta el `:`, contra `bases` del pack: nunca un prefijo, porque
|
|
13
|
+
// `ft:gpt-4o-mini-2024-07-18:...` no es un fine-tuned de `gpt-4` y se apaga en otra fecha.
|
|
14
|
+
const FINE_TUNE = /["'`]ft:([\w.-]+):/g;
|
|
11
15
|
|
|
12
16
|
// Acepta el formato nuevo ({ date, replacement }) y el viejo (solo la fecha).
|
|
13
17
|
const entry = (v) => (typeof v === 'string' ? { date: v, replacement: null } : v);
|
|
@@ -17,6 +21,8 @@ export default function detect({ source, file, pack, today = new Date().toISOStr
|
|
|
17
21
|
const findings = [];
|
|
18
22
|
const usages = [];
|
|
19
23
|
const { retired_models: retired, retirement_floors: floors, retired_endpoints: endpoints } = pack.detect;
|
|
24
|
+
const fineTunes = new Map();
|
|
25
|
+
for (const [name, e] of Object.entries(pack.detect.retired_fine_tunes || {})) for (const b of e.bases) fineTunes.set(b, { name, ...e });
|
|
20
26
|
const azure = AZURE.test(source);
|
|
21
27
|
const lines = source.split('\n');
|
|
22
28
|
lines.forEach((l, i) => {
|
|
@@ -34,6 +40,18 @@ export default function detect({ source, file, pack, today = new Date().toISOStr
|
|
|
34
40
|
findings.push({ pack: PACK, file, line, status: 'detect-only', message });
|
|
35
41
|
usages.push({ provider: 'ia', op: `modelo ${model}`, line });
|
|
36
42
|
}
|
|
43
|
+
for (const [, base] of l.matchAll(FINE_TUNE)) {
|
|
44
|
+
const ft = fineTunes.get(base);
|
|
45
|
+
if (!ft) continue;
|
|
46
|
+
const past = ft.date <= today;
|
|
47
|
+
const when = past ? `fue retirado el ${ft.date}` : `se retira el ${ft.date}`;
|
|
48
|
+
const repl = `OpenAI recomienda como modelo base de reemplazo \`${ft.replacement}\`; el modelo fine-tuned no se migra solo.`;
|
|
49
|
+
const message = inCall
|
|
50
|
+
? `El modelo fine-tuned sobre \`${base}\` (\`${ft.name}\`) ${when}${past ? ': las llamadas ya fallan' : ''}. ${repl} Corré tus pruebas de prompts antes de cambiarlo.`
|
|
51
|
+
: `Aparece un modelo fine-tuned sobre \`${base}\` (\`${ft.name}\`, ${when}) fuera de una llamada: si es una lista o un catálogo de modelos, actualizalo. ${repl}`;
|
|
52
|
+
findings.push({ pack: PACK, file, line, status: 'detect-only', message });
|
|
53
|
+
usages.push({ provider: 'ia', op: `modelo fine-tuned ${base}`, line });
|
|
54
|
+
}
|
|
37
55
|
for (const [model, date] of Object.entries(floors)) {
|
|
38
56
|
if (!l.includes(model)) continue;
|
|
39
57
|
findings.push({ pack: PACK, file, line, status: 'detect-only',
|
package/src/engine.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { loadPacks, transformFor, languageOf } from './packs.js';
|
|
5
|
-
import { SKIP_DIRS, toPosix } from './paths.js';
|
|
5
|
+
import { SKIP_DIRS, toPosix, isBackupDir, isBackupFile } from './paths.js';
|
|
6
6
|
import { scanJs } from './inventory/js-scan.js';
|
|
7
7
|
import { readManifests, buildInventory } from './inventory/index.js';
|
|
8
8
|
import { buildContext } from './context.js';
|
|
@@ -10,18 +10,25 @@ import { buildContext } from './context.js';
|
|
|
10
10
|
// JS/TS y PHP (codemods), WSDL (detección de esquemas viejos) y Python, C#, Java, Go, VB.NET (solo detección).
|
|
11
11
|
const EXT = /\.(m?js|cjs|jsx|ts|tsx|php|wsdl|py|cs|java|go|vb)$/i;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Archivos que el motor lee. Las copias de respaldo (BAK/, "x copy.php"…) no se leen ni se editan:
|
|
14
|
+
// van a `skipped`, con "/", para decir en el reporte qué se ignoró.
|
|
15
|
+
export function listFiles(dir, skipped = []) {
|
|
14
16
|
const out = [];
|
|
15
17
|
const walk = (d) => {
|
|
16
18
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
19
|
+
const rel = () => toPosix(path.relative(dir, path.join(d, e.name)));
|
|
17
20
|
if (e.isDirectory()) {
|
|
18
|
-
if (
|
|
21
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
22
|
+
if (isBackupDir(e.name)) { skipped.push(`${rel()}/`); continue; }
|
|
23
|
+
walk(path.join(d, e.name));
|
|
19
24
|
} else if (EXT.test(e.name) && !/\.d\.ts$/.test(e.name)) {
|
|
20
|
-
|
|
25
|
+
if (isBackupFile(e.name)) skipped.push(rel());
|
|
26
|
+
else out.push(path.join(d, e.name));
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
};
|
|
24
30
|
walk(dir);
|
|
31
|
+
skipped.sort();
|
|
25
32
|
// Mismo orden en Windows y en Linux: se ordena por la ruta con "/".
|
|
26
33
|
return out.sort((a, b) => (toPosix(a) < toPosix(b) ? -1 : 1));
|
|
27
34
|
}
|
|
@@ -31,7 +38,8 @@ export function run(dir, { write = false, packIds = null, today } = {}) {
|
|
|
31
38
|
const packs = loadPacks().filter((p) => !packIds || packIds.includes(p.id));
|
|
32
39
|
const manifests = readManifests(root);
|
|
33
40
|
const deps = { ...manifests.npm, ...manifests.composer };
|
|
34
|
-
const
|
|
41
|
+
const skipped = [];
|
|
42
|
+
const files = listFiles(root, skipped);
|
|
35
43
|
const rels = files.map((abs) => toPosix(path.relative(root, abs)));
|
|
36
44
|
// Lo que un archivo solo no sabe (versiones instaladas, si el repo ya usa el campo, WSDL viejos).
|
|
37
45
|
const context = buildContext(root, rels, manifests);
|
|
@@ -83,7 +91,7 @@ export function run(dir, { write = false, packIds = null, today } = {}) {
|
|
|
83
91
|
providers: Object.fromEntries(Object.entries(providers).map(([k, v]) => [k, { ...v, files: [...v.files] }])),
|
|
84
92
|
usages,
|
|
85
93
|
};
|
|
86
|
-
return { root, packs, context, inventory, findings, changed: [...changed.entries()].map(([file, packIds]) => ({ file, packs: packIds })) };
|
|
94
|
+
return { root, packs, context, inventory, findings, skipped, changed: [...changed.entries()].map(([file, packIds]) => ({ file, packs: packIds })) };
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
export const STATUS_ORDER = ['blocked', 'needs-input', 'needs-review', 'auto', 'detect-only'];
|
package/src/git.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// En Windows, sin core.longpaths, git ve modificado un archivo con ruta de más de 260 caracteres, y `parche pr`
|
|
4
|
+
// frenaba con "Hay cambios sin commitear" en un clon recién hecho (frente B, punto 20).
|
|
5
|
+
export const gitArgs = (args, platform = process.platform) => (platform === 'win32' ? ['-c', 'core.longpaths=true', ...args] : args);
|
|
6
|
+
const git = (cwd, ...args) => execFileSync('git', gitArgs(args), { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
4
7
|
|
|
5
8
|
export function isRepo(cwd) {
|
|
6
9
|
try {
|
|
@@ -48,6 +51,17 @@ export function commitIdentity(cwd, env = process.env) {
|
|
|
48
51
|
throw new Error('No hay una identidad de git para firmar el commit: configurá `git config user.name` y `git config user.email`, o PARCHE_GIT_NAME y PARCHE_GIT_EMAIL.');
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
// --author "Nombre <email>": quién firma, sin tocar la configuración de git de la máquina (frente B, punto 20:
|
|
55
|
+
// la identidad global de la compu era otra persona con un mail personal).
|
|
56
|
+
export function parseAuthor(text) {
|
|
57
|
+
const m = /^\s*(.+?)\s*<([^<>\s]+@[^<>\s]+)>\s*$/.exec(typeof text === 'string' ? text : '');
|
|
58
|
+
if (!m) throw new Error('--author tiene que ser "Nombre <email>", por ejemplo --author "Ana Pérez <ana@users.noreply.github.com>".');
|
|
59
|
+
return { PARCHE_GIT_NAME: m[1], PARCHE_GIT_EMAIL: m[2] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// El email del commit queda público en el PR: si no es un noreply de GitHub, `parche pr` avisa antes de pushear.
|
|
63
|
+
export const isNoreply = (email) => /@users\.noreply\.github\.com$/i.test(email || '');
|
|
64
|
+
|
|
51
65
|
export function commitAll(cwd, message, env = process.env) {
|
|
52
66
|
const id = commitIdentity(cwd, env);
|
|
53
67
|
git(cwd, 'add', '-A');
|
package/src/paths.js
CHANGED
|
@@ -16,9 +16,19 @@ export function isTestFile(file) {
|
|
|
16
16
|
|| /\.(test|spec|mock)\.[cm]?[jt]sx?$/i.test(file) || /Test\.php$/.test(file);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
19
|
+
// Copias de respaldo: no es el código que corre. Un PR sobre BAK/ o sobre "factura copy.php" no le sirve al
|
|
20
|
+
// mantenedor (frente B, 17/10: axel-tecnosoul/facturador ya usaba el campo en los archivos vigentes).
|
|
21
|
+
// Carpetas BAK, backup(s), old, _old y deprecated; archivos "x copy.php", "x - Copy.js", "x copia.php",
|
|
22
|
+
// "x.bak.php", "x-old.js", "x_old.php" y "x.old.php". No cuentan "deep_copy.js" ni "gold.js".
|
|
23
|
+
const BACKUP_DIR = /^(bak|backups?|old|_old|deprecated)$/i;
|
|
24
|
+
export const isBackupDir = (name) => BACKUP_DIR.test(name);
|
|
25
|
+
export function isBackupFile(name) {
|
|
26
|
+
return /\s(-\s)?(copy|copia)(\s\(?\d+\)?)?\.[^.]+$/i.test(name) || /\.bak\.[^.]+$/i.test(name) || /[-_.]old\.[^.]+$/i.test(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Recorre el repo (salteando SKIP_DIRS y las copias de respaldo) y devuelve las rutas relativas, con "/",
|
|
30
|
+
// de los archivos que cumplen `match(nombre)`. Lo salteado por ser respaldo va a `skipped`, si se pasa.
|
|
31
|
+
export function findFiles(root, match, skipped = null) {
|
|
22
32
|
const out = [];
|
|
23
33
|
const walk = (dir) => {
|
|
24
34
|
let entries;
|
|
@@ -28,8 +38,15 @@ export function findFiles(root, match) {
|
|
|
28
38
|
return;
|
|
29
39
|
}
|
|
30
40
|
for (const e of entries) {
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
const rel = () => toPosix(path.relative(root, path.join(dir, e.name)));
|
|
42
|
+
if (e.isDirectory()) {
|
|
43
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
44
|
+
if (isBackupDir(e.name)) { skipped?.push(`${rel()}/`); continue; }
|
|
45
|
+
walk(path.join(dir, e.name));
|
|
46
|
+
} else if (match(e.name)) {
|
|
47
|
+
if (isBackupFile(e.name)) skipped?.push(rel());
|
|
48
|
+
else out.push(rel());
|
|
49
|
+
}
|
|
33
50
|
}
|
|
34
51
|
};
|
|
35
52
|
walk(root);
|
package/src/report.js
CHANGED
|
@@ -53,7 +53,7 @@ function upgradesSection(upgrades) {
|
|
|
53
53
|
return out;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function scanReport({ inventory, findings, packs, changed, upgrades = null }, today = new Date()) {
|
|
56
|
+
export function scanReport({ inventory, findings, packs, changed, upgrades = null, skipped = [] }, today = new Date()) {
|
|
57
57
|
const out = [];
|
|
58
58
|
out.push(`# Parche — reporte de \`${inventory.repo}\``, '');
|
|
59
59
|
out.push('## APIs que usa este repo', '');
|
|
@@ -73,6 +73,10 @@ export function scanReport({ inventory, findings, packs, changed, upgrades = nul
|
|
|
73
73
|
out.push('**Llamadas HTTP con URL dinámica** (no se puede saber a qué host van):', '',
|
|
74
74
|
...inventory.unresolved.map((u) => `- \`${u.file}:${u.line}\` (${u.client})`), '');
|
|
75
75
|
}
|
|
76
|
+
if (skipped.length) {
|
|
77
|
+
out.push('**Copias de respaldo que no se revisaron** (si alguna es código que corre, renombrala o movela):', '',
|
|
78
|
+
...skipped.map((s) => `- \`${s}\``), '');
|
|
79
|
+
}
|
|
76
80
|
if (upgrades) out.push(...upgradesSection(upgrades));
|
|
77
81
|
for (const pack of packs) {
|
|
78
82
|
const fs = findings.filter((f) => f.pack === pack.id);
|