changebook 0.6.0 → 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/dist/aciertos.js +100 -0
- package/dist/aliasDeModulo.js +203 -0
- package/dist/badge.js +159 -0
- package/dist/context.js +7 -1
- package/dist/guard.js +96 -13
- package/dist/hookMudo.js +172 -0
- package/dist/impact.js +319 -25
- package/dist/import.js +26 -1
- package/dist/index.js +173 -5
- package/dist/pregunta.js +59 -0
- package/dist/reglas.js +299 -0
- package/dist/scan.js +427 -0
- package/dist/supabase.js +84 -1
- package/dist/sync.js +179 -24
- package/dist/tools.js +182 -44
- package/package.json +2 -2
- package/server.json +3 -3
package/dist/sync.js
CHANGED
|
@@ -12,6 +12,28 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
+
import { aliasesFor, canonicalizeModuleRows, etiquetaPorSlug, projectIdOf, slugModule, } from './aliasDeModulo.js';
|
|
16
|
+
/**
|
|
17
|
+
* Cuántas citas de historial sirve el bloque, según el plan (B5, opción A).
|
|
18
|
+
*
|
|
19
|
+
* FALLA HACIA EL PLAN GRATUITO, SIEMPRE. Un error de red, una tabla que no
|
|
20
|
+
* responde o una fila que no existe devuelven el gratuito. Al revés —regalar el
|
|
21
|
+
* de pago cuando algo falla— es un muro que se cae solo el día que hay una
|
|
22
|
+
* incidencia, y nadie se entera de que se cayó.
|
|
23
|
+
*
|
|
24
|
+
* `user_plans` está acotada por RLS al usuario de la sesión, así que no hace
|
|
25
|
+
* falta filtrar por id: si hay fila, es la suya.
|
|
26
|
+
*/
|
|
27
|
+
export async function citasSegunPlan(db) {
|
|
28
|
+
try {
|
|
29
|
+
const filas = await db.rest('user_plans?select=plan&limit=1');
|
|
30
|
+
const plan = (filas[0]?.plan ?? 'free').trim().toLowerCase();
|
|
31
|
+
return plan && plan !== 'free' ? CITAS_PRO : CITAS_GRATIS;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return CITAS_GRATIS;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
/** The project identity of the synced repo (same rule as analyze/guard). */
|
|
16
38
|
function projectNameFor(targetDir) {
|
|
17
39
|
return (process.env.CHANGEBOOK_PROJECT?.trim() ||
|
|
@@ -19,6 +41,11 @@ function projectNameFor(targetDir) {
|
|
|
19
41
|
}
|
|
20
42
|
const START = '<!-- changebook:start -->';
|
|
21
43
|
const END = '<!-- changebook:end -->';
|
|
44
|
+
// La firma viaja con cada clone y cada PR: el bloque vive en el CLAUDE.md del
|
|
45
|
+
// usuario y todo colaborador (o su agente) que lo abra la ve. Una línea, sin
|
|
46
|
+
// emoji, sin mayúsculas, sin enlace markdown. Se RESERVA del presupuesto antes
|
|
47
|
+
// de repartirlo, así que si algo se recorta es el contenido, nunca la firma.
|
|
48
|
+
const FIRMA = '_Generado por ChangeBook · changebook.app_';
|
|
22
49
|
// DB text (notes, alert bodies, business impact) is AI-written and gets embedded
|
|
23
50
|
// between the markers that upsertSection splices on. If any of it contained a
|
|
24
51
|
// literal marker, the next sync would splice at the wrong offset and corrupt the
|
|
@@ -95,9 +122,9 @@ export async function fetchBriefSection(db, targetDir) {
|
|
|
95
122
|
projectFilter = '&project_id=eq.00000000-0000-0000-0000-000000000000';
|
|
96
123
|
projectResolved = false;
|
|
97
124
|
}
|
|
98
|
-
const projectId =
|
|
125
|
+
const projectId = projectIdOf(projectFilter);
|
|
99
126
|
const since = new Date(Date.now() - ALERT_WINDOW_DAYS * 24 * 3600 * 1000).toISOString();
|
|
100
|
-
const [
|
|
127
|
+
const [moduleRowsCrudas, changes, alertsCrudas, historialCrudo, pendingTasks, healthRows, { aliases },] = await Promise.all([
|
|
101
128
|
db.rest('change_module?select=changelog_id,module,domain,risk,files,note,created_at&order=created_at.desc&limit=500' +
|
|
102
129
|
projectFilter),
|
|
103
130
|
db.rest(`changelog?select=business_impact,created_at&order=created_at.desc&limit=${MAX_CHANGES}` +
|
|
@@ -109,7 +136,7 @@ export async function fetchBriefSection(db, targetDir) {
|
|
|
109
136
|
// HISTORIA de regresiones: todas, sin ventana y sin filtrar por abiertas.
|
|
110
137
|
// Es lo que sustituye al mapa de modulos en el bloque — ver `hechosCaros`.
|
|
111
138
|
db
|
|
112
|
-
.rest('regression_alerts?select=module,plain,created_at,changelog_id&order=created_at.desc&limit=500' +
|
|
139
|
+
.rest('regression_alerts?select=module,plain,resolution,created_at,changelog_id&order=created_at.desc&limit=500' +
|
|
113
140
|
projectFilter)
|
|
114
141
|
.catch(() => []),
|
|
115
142
|
projectResolved && projectId
|
|
@@ -125,7 +152,19 @@ export async function fetchBriefSection(db, targetDir) {
|
|
|
125
152
|
.rest('project_checks?select=check_id,status,evidence,updated_at&order=updated_at.desc&limit=8' +
|
|
126
153
|
projectFilter)
|
|
127
154
|
.catch(() => []),
|
|
155
|
+
// Los alias entran en el MISMO Promise.all que ya se hacía: no cuesta ronda.
|
|
156
|
+
aliasesFor(db, projectId),
|
|
128
157
|
]);
|
|
158
|
+
// El punto de estrangulamiento del bloque: se canonicaliza AQUÍ, nada más
|
|
159
|
+
// traer las filas, y todo lo que hay debajo (el mapa, `hechosCaros`,
|
|
160
|
+
// `coChangePairs`, los avisos) sigue siendo puro y agrupa por nombre sin
|
|
161
|
+
// enterarse. Las TRES tablas, no solo el mapa: el bloque de CLAUDE.md cuenta
|
|
162
|
+
// regresiones por módulo, así que una fusión que resolviera el mapa y no el
|
|
163
|
+
// historial diría «3 regresiones» de un módulo y las listaría bajo otro
|
|
164
|
+
// nombre, en la misma pantalla.
|
|
165
|
+
const moduleRows = canonicalizeModuleRows(moduleRowsCrudas, aliases);
|
|
166
|
+
const alerts = canonicalizeModuleRows(alertsCrudas, aliases);
|
|
167
|
+
const historial = canonicalizeModuleRows(historialCrudo, aliases);
|
|
129
168
|
const health = summarizeHealth(healthRows);
|
|
130
169
|
// El commit de cada regresion, para poder citarlo. Una consulta mas, y solo
|
|
131
170
|
// por los analisis que de verdad rompieron algo: es la diferencia entre «este
|
|
@@ -147,9 +186,47 @@ export async function fetchBriefSection(db, targetDir) {
|
|
|
147
186
|
commitPorAnalisis.set(f.id, f.commit_hash.slice(0, 7));
|
|
148
187
|
}
|
|
149
188
|
}
|
|
150
|
-
const section = buildSection(moduleRows, changes, alerts, projectName, pendingTasks, health.at_risk, historial, commitPorAnalisis);
|
|
189
|
+
const section = buildSection(moduleRows, changes, alerts, projectName, pendingTasks, health.at_risk, historial, commitPorAnalisis, await citasSegunPlan(db));
|
|
151
190
|
return { section, projectId, projectResolved };
|
|
152
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Lo que va a cambiar, en corto y para una persona.
|
|
194
|
+
*
|
|
195
|
+
* Pura a propósito: la decisión de qué enseñar se prueba sin terminal y sin
|
|
196
|
+
* escribir en disco. El diff entero se deja para quien lo pida — el objetivo de
|
|
197
|
+
* B7 no es volcar un diff, es que quien instala vea que su fichero se respeta.
|
|
198
|
+
*/
|
|
199
|
+
export function resumenDelCambio(previo, siguiente, fichero) {
|
|
200
|
+
const nombre = fichero.split('/').pop() ?? fichero;
|
|
201
|
+
if (previo === null) {
|
|
202
|
+
return `Voy a crear ${nombre} (${siguiente.split('\n').length} líneas).`;
|
|
203
|
+
}
|
|
204
|
+
const antes = previo.split('\n');
|
|
205
|
+
const despues = siguiente.split('\n');
|
|
206
|
+
const comunes = new Set(antes);
|
|
207
|
+
const anadidas = despues.filter((l) => l.trim() && !comunes.has(l));
|
|
208
|
+
const vivas = new Set(despues);
|
|
209
|
+
const quitadas = antes.filter((l) => l.trim() && !vivas.has(l));
|
|
210
|
+
// LO QUE MÁS IMPORTA DE ESTE MENSAJE es la primera línea: que el fichero de
|
|
211
|
+
// quien lo lee NO se toca fuera del bloque. Es lo que el producto ya hacía
|
|
212
|
+
// bien y no contaba — y la desconfianza no viene de lo que haces, viene de lo
|
|
213
|
+
// que no se ve.
|
|
214
|
+
const fuera = antes.filter((l) => vivas.has(l)).length;
|
|
215
|
+
const l = [
|
|
216
|
+
`He encontrado tu ${nombre} (${antes.length} líneas). Conservo ${fuera} tal cual`,
|
|
217
|
+
`y solo reescribo el bloque entre los marcadores de ChangeBook:`,
|
|
218
|
+
'',
|
|
219
|
+
];
|
|
220
|
+
for (const linea of anadidas.slice(0, 4))
|
|
221
|
+
l.push(` + ${linea.slice(0, 90)}`);
|
|
222
|
+
if (anadidas.length > 4)
|
|
223
|
+
l.push(` + … y ${anadidas.length - 4} líneas más`);
|
|
224
|
+
for (const linea of quitadas.slice(0, 2))
|
|
225
|
+
l.push(` - ${linea.slice(0, 90)}`);
|
|
226
|
+
if (quitadas.length > 2)
|
|
227
|
+
l.push(` - … y ${quitadas.length - 2} líneas más`);
|
|
228
|
+
return l.join('\n');
|
|
229
|
+
}
|
|
153
230
|
export async function syncContextFiles(db, targetDir, opts = {}) {
|
|
154
231
|
const { section, projectId, projectResolved } = await fetchBriefSection(db, targetDir);
|
|
155
232
|
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
@@ -199,9 +276,31 @@ export async function syncContextFiles(db, targetDir, opts = {}) {
|
|
|
199
276
|
* `computeRecidivism`: la misma regresion re-levantada tres veces es UNA, y
|
|
200
277
|
* contarla tres veces convertiria el churn del generador en falsa gravedad.
|
|
201
278
|
*/
|
|
202
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Citas por módulo en el plan gratuito. La línea es una pista para ir a mirar,
|
|
281
|
+
* no un informe.
|
|
282
|
+
*/
|
|
283
|
+
export const CITAS_GRATIS = 3;
|
|
284
|
+
/**
|
|
285
|
+
* Y en el de pago (B5, opción A: nadie pierde nada, el que paga gana).
|
|
286
|
+
*
|
|
287
|
+
* ACOTADO, NO ILIMITADO, y el motivo está escrito dos veces en este fichero: el
|
|
288
|
+
* bloque viaja en CADA petición de CADA sesión del usuario, así que es coste
|
|
289
|
+
* FIJO, y `SYNC_BUDGET_CHARS` existe porque ya se intentó subir el techo en vez
|
|
290
|
+
* de adelgazar el contenido y NO funcionó — la sección «Módulos» se quedó fuera
|
|
291
|
+
* igual. «El que paga lo ve todo» sonaría mejor y le costaría tokens en cada
|
|
292
|
+
* turno para leer diez fechas que no va a mirar.
|
|
293
|
+
*/
|
|
294
|
+
export const CITAS_PRO = 8;
|
|
295
|
+
export function hechosCaros(historial, commitPorAnalisis, tope = 6, maxCitas = CITAS_GRATIS) {
|
|
203
296
|
const porModulo = new Map();
|
|
204
297
|
for (const h of historial) {
|
|
298
|
+
// SOLO LAS CONFIRMADAS, igual que `computeRecidivism`. Esta es la sección
|
|
299
|
+
// que el dueño ve en su CLAUDE.md, así que dejarla contando avisos mientras
|
|
300
|
+
// la tool cuenta regresiones daría DOS cifras distintas para la misma
|
|
301
|
+
// pregunta — que es peor que una cifra mala.
|
|
302
|
+
if ((h.resolution ?? '').trim().toLowerCase() !== 'fixed')
|
|
303
|
+
continue;
|
|
205
304
|
const modulo = (h.module ?? '').trim();
|
|
206
305
|
const texto = (h.plain ?? '').trim();
|
|
207
306
|
if (!modulo || !texto)
|
|
@@ -223,24 +322,37 @@ export function hechosCaros(historial, commitPorAnalisis, tope = 6) {
|
|
|
223
322
|
.slice(0, tope)
|
|
224
323
|
.map(([modulo, { textos, citas }]) => {
|
|
225
324
|
const n = textos.size;
|
|
226
|
-
//
|
|
227
|
-
// informe. Con mas, la seccion se come el presupuesto del bloque.
|
|
325
|
+
// Cuántas citas caben: ver CITAS_GRATIS / CITAS_PRO.
|
|
228
326
|
const cuando = citas
|
|
229
|
-
.slice(0,
|
|
327
|
+
.slice(0, maxCitas)
|
|
230
328
|
.map((c) => (c.commit ? `${c.fecha} \`${c.commit}\`` : c.fecha))
|
|
231
329
|
.join(', ');
|
|
232
330
|
return `- **${modulo}**: ${n} regresi${n === 1 ? 'ón' : 'ones'}${cuando ? ` (${cuando})` : ''}`;
|
|
233
331
|
});
|
|
234
332
|
}
|
|
235
|
-
export function buildSection(rows, changes, alerts = [], projectName, pendingTasks = [], atRiskHealth = [], historial = [], commitPorAnalisis = new Map()
|
|
333
|
+
export function buildSection(rows, changes, alerts = [], projectName, pendingTasks = [], atRiskHealth = [], historial = [], commitPorAnalisis = new Map(),
|
|
334
|
+
// B5 · el que paga ve más historial de regresiones. Por defecto, el gratuito:
|
|
335
|
+
// un fallo al leer el plan tiene que degradar HACIA el plan libre, nunca
|
|
336
|
+
// regalar el de pago por un error de red.
|
|
337
|
+
maxCitas = CITAS_GRATIS) {
|
|
236
338
|
// Newest-first rows: the first occurrence of a module is its latest state.
|
|
339
|
+
//
|
|
340
|
+
// POR SLUG (03/08). Agrupaba por `row.module` crudo, asi que un modulo
|
|
341
|
+
// escrito de dos maneras salia DOS VECES en el bloque que se escribe en
|
|
342
|
+
// CLAUDE.md/AGENTS.md — y ademas gastaba dos de las MAX_MODULES plazas, o
|
|
343
|
+
// sea que expulsaba a un modulo real. Este fichero ya agrupaba por slug en
|
|
344
|
+
// otros dos sitios; esta agregacion se habia quedado fuera, que es la forma
|
|
345
|
+
// exacta de la invariante 16: media normalizacion se lee igual que una
|
|
346
|
+
// entera.
|
|
347
|
+
const etiqueta = etiquetaPorSlug(rows.map((r) => r.module ?? ''));
|
|
237
348
|
const seen = new Map();
|
|
238
349
|
for (const row of rows) {
|
|
239
|
-
const
|
|
350
|
+
const clave = slugModule((row.module ?? '').trim());
|
|
351
|
+
const existing = seen.get(clave);
|
|
240
352
|
if (existing)
|
|
241
353
|
existing.count += 1;
|
|
242
354
|
else
|
|
243
|
-
seen.set(
|
|
355
|
+
seen.set(clave, { ...row, module: etiqueta.get(clave) ?? row.module, count: 1 });
|
|
244
356
|
}
|
|
245
357
|
const modules = [...seen.values()].slice(0, MAX_MODULES);
|
|
246
358
|
const head = [
|
|
@@ -294,7 +406,16 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
|
|
|
294
406
|
// tokens y CERO llamadas al atlas. «editar un archivo» no casaba con cómo
|
|
295
407
|
// habla quien programa con un agente. Quien no pregunta nada no reconoce un
|
|
296
408
|
// disparador abstracto: hay que nombrar sus verbos.
|
|
297
|
-
|
|
409
|
+
//
|
|
410
|
+
// Y el 2026-08-02 se vio la MITAD QUE FALTABA. Habia disparador para
|
|
411
|
+
// registrar cambios y ninguno para juzgar avisos, y el resultado esta
|
|
412
|
+
// medido: de 119 alertas, 79 sin motivo ninguno. No es que nadie las
|
|
413
|
+
// mirara — mirar y APUNTAR son dos actos distintos y solo uno era gratis.
|
|
414
|
+
// Ese dia yo mismo verifique un aviso, lo arregle y no lo cerre hasta que
|
|
415
|
+
// el dueño me pregunto por que seguia abierto. `atlas_resolve_alert` entra
|
|
416
|
+
// con sus verbos («comprobar», «arreglar») por la misma ley de arriba, y
|
|
417
|
+
// pagando su precio: cada char aqui le quita sitio al mapa.
|
|
418
|
+
'Disparador → tool: orientarte → `atlas_project_brief` (`intent:"orient"`) · antes de editar/cambiar/arreglar/refactorizar un archivo o función → `atlas_file_context` (dice QUIÉN DEPENDE de eso) · riesgos/acoplamientos → el brief. Tras cada commit: `atlas_record_change` (diff, hash, fecha, `summary` tuyo; reenviar es gratis). Al comprobar o arreglar un aviso: `atlas_resolve_alert` (si era falso, `verdict:"dismissed"`).',
|
|
298
419
|
'',
|
|
299
420
|
'Tools diferidas, en UNA llamada: ToolSearch `select:mcp__changebook__atlas_project_brief,mcp__changebook__atlas_file_context`.',
|
|
300
421
|
'',
|
|
@@ -424,7 +545,7 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
|
|
|
424
545
|
key: 'costoso',
|
|
425
546
|
priority: 1,
|
|
426
547
|
title: '### Lo que ya costó caro aquí (revisa antes de tocarlo)',
|
|
427
|
-
lines: hechosCaros(historial, commitPorAnalisis),
|
|
548
|
+
lines: hechosCaros(historial, commitPorAnalisis, 6, maxCitas),
|
|
428
549
|
},
|
|
429
550
|
// El mapa baja de 3 a 4, y no es una degradación caprichosa: `/doctor` de
|
|
430
551
|
// Claude Code recorta por su cuenta «architecture overviews» de un
|
|
@@ -439,7 +560,10 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
|
|
|
439
560
|
lines: changeLines,
|
|
440
561
|
},
|
|
441
562
|
];
|
|
442
|
-
|
|
563
|
+
// La firma se aparta del fondo ANTES del reparto (línea en blanco separadora
|
|
564
|
+
// + la propia línea): lo que compita por presupuesto lo hace sobre lo que
|
|
565
|
+
// sobra, de modo que el recorte cae siempre en el contenido, nunca en la firma.
|
|
566
|
+
let budget = SYNC_BUDGET_CHARS - head.join('\n').length - END.length - (FIRMA.length + 2);
|
|
443
567
|
// ── El suelo del mapa ──────────────────────────────────────────────────────
|
|
444
568
|
//
|
|
445
569
|
// El presupuesto es de suma cero, así que poner el riesgo primero se lo come.
|
|
@@ -524,6 +648,9 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
|
|
|
524
648
|
lines.push(s.title, ...s.lines.slice(0, count));
|
|
525
649
|
first = false;
|
|
526
650
|
}
|
|
651
|
+
// La firma, al final del bloque y antes del marcador de cierre. Su hueco ya
|
|
652
|
+
// se apartó del presupuesto arriba, así que entra siempre.
|
|
653
|
+
lines.push('', FIRMA);
|
|
527
654
|
lines.push(END);
|
|
528
655
|
return lines.join('\n');
|
|
529
656
|
}
|
|
@@ -571,11 +698,17 @@ const ES_FICHERO_DE_PRUEBAS = /(^|\/)(tests?|__tests__|spec)\/|\.(test|spec)\.[A
|
|
|
571
698
|
*/
|
|
572
699
|
const RATIO_MODULO_DE_PRUEBAS = 0.8;
|
|
573
700
|
export function modulosDePruebas(rows) {
|
|
701
|
+
// Por slug: si el módulo va partido en dos etiquetas, cada mitad se evaluaría
|
|
702
|
+
// contra el umbral por su cuenta y una podría pasar por «de pruebas» mientras
|
|
703
|
+
// la otra no. El Set devuelto lleva la etiqueta representante, que es la
|
|
704
|
+
// misma que usa coChangePairs, para que el `dePruebas.has(...)` de allí case.
|
|
705
|
+
const etiqueta = etiquetaPorSlug(rows.map((r) => r.module ?? ''));
|
|
574
706
|
const porModulo = new Map();
|
|
575
707
|
for (const r of rows) {
|
|
576
|
-
const
|
|
577
|
-
if (!
|
|
708
|
+
const crudo = (r.module ?? '').trim();
|
|
709
|
+
if (!crudo || !Array.isArray(r.files))
|
|
578
710
|
continue;
|
|
711
|
+
const label = etiqueta.get(slugModule(crudo)) ?? crudo;
|
|
579
712
|
const set = porModulo.get(label) ?? new Set();
|
|
580
713
|
for (const f of r.files) {
|
|
581
714
|
if (typeof f === 'string' && f.trim())
|
|
@@ -597,11 +730,19 @@ export function modulosDePruebas(rows) {
|
|
|
597
730
|
return fuera;
|
|
598
731
|
}
|
|
599
732
|
export function coChangePairs(rows) {
|
|
733
|
+
// LA ARISTA QUE FALTA NO LA VE NADIE. Un módulo partido en dos etiquetas
|
|
734
|
+
// reparte sus apariciones entre las dos mitades: con 13+1 se pierde poco,
|
|
735
|
+
// pero con un reparto parejo —8 y 6 contra un umbral de 10— las DOS mitades
|
|
736
|
+
// caen por debajo de MIN_PAIR_COUNT y el acoplamiento real desaparece del
|
|
737
|
+
// grafo. El fantasma del brief se ve y se denuncia; esto no lo nota nadie
|
|
738
|
+
// nunca, y es justo lo que el producto promete enseñar.
|
|
739
|
+
const etiqueta = etiquetaPorSlug(rows.map((r) => r.module ?? ''));
|
|
600
740
|
const byAnalysis = new Map();
|
|
601
741
|
for (const r of rows) {
|
|
602
|
-
const
|
|
603
|
-
if (!
|
|
742
|
+
const crudo = (r.module ?? '').trim();
|
|
743
|
+
if (!crudo)
|
|
604
744
|
continue;
|
|
745
|
+
const label = etiqueta.get(slugModule(crudo)) ?? crudo;
|
|
605
746
|
let set = byAnalysis.get(r.changelog_id);
|
|
606
747
|
if (!set) {
|
|
607
748
|
set = new Set();
|
|
@@ -647,11 +788,25 @@ export async function upsertSection(file, section, opts = {}) {
|
|
|
647
788
|
catch {
|
|
648
789
|
content = null;
|
|
649
790
|
}
|
|
791
|
+
/** Escribe, salvo que quien mira diga que no. */
|
|
792
|
+
const escribir = async (siguiente, resultado) => {
|
|
793
|
+
if (opts.confirmar) {
|
|
794
|
+
const ok = await opts.confirmar({
|
|
795
|
+
fichero: file,
|
|
796
|
+
previo: content,
|
|
797
|
+
siguiente,
|
|
798
|
+
resumen: resumenDelCambio(content, siguiente, file),
|
|
799
|
+
});
|
|
800
|
+
if (!ok)
|
|
801
|
+
return 'skipped';
|
|
802
|
+
}
|
|
803
|
+
await writeFile(file, siguiente);
|
|
804
|
+
return resultado;
|
|
805
|
+
};
|
|
650
806
|
if (content === null) {
|
|
651
807
|
if (opts.refreshOnly)
|
|
652
808
|
return 'skipped';
|
|
653
|
-
|
|
654
|
-
return 'created';
|
|
809
|
+
return escribir(section + '\n', 'created');
|
|
655
810
|
}
|
|
656
811
|
// Migración del renombrado (AppAtlas → ChangeBook): si el archivo aún tiene
|
|
657
812
|
// el bloque con los marcadores antiguos, elimínalo antes de upsertar el
|
|
@@ -679,13 +834,13 @@ export async function upsertSection(file, section, opts = {}) {
|
|
|
679
834
|
const next = content.slice(0, start) + section + content.slice(end + END.length);
|
|
680
835
|
// Idempotente: si el mapa no cambió, no tocar el archivo — reescribirlo
|
|
681
836
|
// actualizaría el mtime, ensuciaría git status y podría invalidar cachés.
|
|
837
|
+
// Y NO SE PREGUNTA cuando no hay nada que cambiar: una pregunta cuya única
|
|
838
|
+
// respuesta útil es «sí, no hagas nada» es ruido que enseña a decir que sí.
|
|
682
839
|
if (next === content)
|
|
683
840
|
return 'unchanged';
|
|
684
|
-
|
|
685
|
-
return 'updated';
|
|
841
|
+
return escribir(next, 'updated');
|
|
686
842
|
}
|
|
687
843
|
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
688
|
-
|
|
689
|
-
return 'appended';
|
|
844
|
+
return escribir(content + separator + section + '\n', 'appended');
|
|
690
845
|
}
|
|
691
846
|
//# sourceMappingURL=sync.js.map
|