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 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
- if (f.partial) console.log(` ${''.padEnd(16)} No cubre: ${f.partial.message}`);
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.commitIdentity(root);
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
- fs.writeFileSync(bodyPath, prBody({ pack, findings: result.findings, changed: result.changed, verification }));
229
- if (!changedFiles.length || changedFiles.every(isTestFile)) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Dependabot para APIs: detecta cambios de APIs de terceros en tu código y abre el PR con el arreglo.",
5
5
  "keywords": [
6
6
  "api",
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "id": "ai.model-retirements",
3
- "version": "2026.10.16",
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-16",
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
- export function listFiles(dir) {
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 (!SKIP_DIRS.has(e.name)) walk(path.join(d, e.name));
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
- out.push(path.join(d, e.name));
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 files = listFiles(root);
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
- const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
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
- // Recorre el repo (salteando SKIP_DIRS) y devuelve las rutas relativas, con "/", de los archivos que
20
- // cumplen `match(nombre)`.
21
- export function findFiles(root, match) {
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
- if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name)); }
32
- else if (match(e.name)) out.push(toPosix(path.relative(root, path.join(dir, e.name))));
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);