changebook 0.5.0 → 0.6.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/sync.js CHANGED
@@ -97,7 +97,7 @@ export async function fetchBriefSection(db, targetDir) {
97
97
  }
98
98
  const projectId = /project_id=eq\.([0-9a-f-]+)/.exec(projectFilter)?.[1] ?? null;
99
99
  const since = new Date(Date.now() - ALERT_WINDOW_DAYS * 24 * 3600 * 1000).toISOString();
100
- const [moduleRows, changes, alerts, pendingTasks, healthRows] = await Promise.all([
100
+ const [moduleRows, changes, alerts, historial, pendingTasks, healthRows] = await Promise.all([
101
101
  db.rest('change_module?select=changelog_id,module,domain,risk,files,note,created_at&order=created_at.desc&limit=500' +
102
102
  projectFilter),
103
103
  db.rest(`changelog?select=business_impact,created_at&order=created_at.desc&limit=${MAX_CHANGES}` +
@@ -106,6 +106,12 @@ export async function fetchBriefSection(db, targetDir) {
106
106
  .rest(`regression_alerts?select=module,plain,created_at&created_at=gte.${since}&resolved_at=is.null&order=created_at.desc&limit=${MAX_ALERTS}` +
107
107
  projectFilter)
108
108
  .catch(() => []),
109
+ // HISTORIA de regresiones: todas, sin ventana y sin filtrar por abiertas.
110
+ // Es lo que sustituye al mapa de modulos en el bloque — ver `hechosCaros`.
111
+ db
112
+ .rest('regression_alerts?select=module,plain,created_at,changelog_id&order=created_at.desc&limit=500' +
113
+ projectFilter)
114
+ .catch(() => []),
109
115
  projectResolved && projectId
110
116
  ? db
111
117
  .callRpc('list_agent_tasks', {
@@ -121,7 +127,27 @@ export async function fetchBriefSection(db, targetDir) {
121
127
  .catch(() => []),
122
128
  ]);
123
129
  const health = summarizeHealth(healthRows);
124
- const section = buildSection(moduleRows, changes, alerts, projectName, pendingTasks, health.at_risk);
130
+ // El commit de cada regresion, para poder citarlo. Una consulta mas, y solo
131
+ // por los analisis que de verdad rompieron algo: es la diferencia entre «este
132
+ // modulo ha roto 3 veces» y «este modulo ha roto 3 veces, aqui, aqui y aqui».
133
+ // Sin el hash, la frase es una etiqueta; con el, es algo que se puede ir a
134
+ // mirar. El sync corre UNA vez por sesion, asi que la ronda extra se paga una
135
+ // vez y no por edicion.
136
+ const idsDeRegresion = [
137
+ ...new Set(historial.map((h) => h.changelog_id).filter((x) => Boolean(x))),
138
+ ].slice(0, 60);
139
+ const commitPorAnalisis = new Map();
140
+ if (idsDeRegresion.length > 0) {
141
+ const filas = await db
142
+ .rest(`changelog?select=id,commit_hash&id=in.(${idsDeRegresion.join(',')})` +
143
+ projectFilter)
144
+ .catch(() => []);
145
+ for (const f of filas) {
146
+ if (f.commit_hash)
147
+ commitPorAnalisis.set(f.id, f.commit_hash.slice(0, 7));
148
+ }
149
+ }
150
+ const section = buildSection(moduleRows, changes, alerts, projectName, pendingTasks, health.at_risk, historial, commitPorAnalisis);
125
151
  return { section, projectId, projectResolved };
126
152
  }
127
153
  export async function syncContextFiles(db, targetDir, opts = {}) {
@@ -146,7 +172,67 @@ export async function syncContextFiles(db, targetDir, opts = {}) {
146
172
  }
147
173
  }
148
174
  /** Exported for tests. */
149
- export function buildSection(rows, changes, alerts = [], projectName, pendingTasks = [], atRiskHealth = []) {
175
+ /**
176
+ * LO QUE YA COSTO CARO AQUI: modulos que han roto algo, con fecha y commit.
177
+ *
178
+ * ── POR QUE ESTO Y NO EL MAPA DE MODULOS ────────────────────────────────────
179
+ *
180
+ * La documentacion de Claude Code lo dice de su propio `/doctor`: recorta del
181
+ * CLAUDE.md «directory layouts, dependency lists, and architecture overviews»
182
+ * y conserva «pitfalls, rationale, and conventions that differ from tool
183
+ * defaults». La lista de modulos con su dominio es DERIVABLE: el agente la saca
184
+ * leyendo el repo, y la herramienta la recorta por su cuenta.
185
+ *
186
+ * Esto no. Que un modulo haya roto tres veces sale del HISTORIAL, no del
187
+ * codigo: ni un grep ni un glob pueden encontrarlo, y es exactamente la
188
+ * categoria que /doctor conserva.
189
+ *
190
+ * ── DOS DECISIONES QUE PARECEN DETALLES ─────────────────────────────────────
191
+ *
192
+ * FECHA Y HASH POR ENTRADA, NUNCA EN LA CABECERA. Una fecha en la cabecera
193
+ * cambiaria a diario e invalidaria el prefijo de prompt-cache de TODO el
194
+ * fichero en cada sesion. Por entrada no: una linea que no cambia no invalida
195
+ * nada, y `upsertSection` sigue devolviendo `unchanged` cuando no ha pasado
196
+ * nada, asi que hechos con fecha no producen churn diario en `git status`.
197
+ *
198
+ * SE CUENTAN PROBLEMAS DISTINTOS, no filas. Mismo criterio que
199
+ * `computeRecidivism`: la misma regresion re-levantada tres veces es UNA, y
200
+ * contarla tres veces convertiria el churn del generador en falsa gravedad.
201
+ */
202
+ export function hechosCaros(historial, commitPorAnalisis, tope = 6) {
203
+ const porModulo = new Map();
204
+ for (const h of historial) {
205
+ const modulo = (h.module ?? '').trim();
206
+ const texto = (h.plain ?? '').trim();
207
+ if (!modulo || !texto)
208
+ continue;
209
+ const entrada = porModulo.get(modulo) ?? { textos: new Set(), citas: [] };
210
+ if (!entrada.textos.has(texto)) {
211
+ entrada.textos.add(texto);
212
+ entrada.citas.push({
213
+ fecha: h.created_at.slice(5, 10).split('-').reverse().join('/'),
214
+ commit: h.changelog_id
215
+ ? (commitPorAnalisis.get(h.changelog_id) ?? null)
216
+ : null,
217
+ });
218
+ }
219
+ porModulo.set(modulo, entrada);
220
+ }
221
+ return [...porModulo.entries()]
222
+ .sort((a, b) => b[1].textos.size - a[1].textos.size || a[0].localeCompare(b[0]))
223
+ .slice(0, tope)
224
+ .map(([modulo, { textos, citas }]) => {
225
+ const n = textos.size;
226
+ // Tres citas como mucho: la linea es una pista para ir a mirar, no un
227
+ // informe. Con mas, la seccion se come el presupuesto del bloque.
228
+ const cuando = citas
229
+ .slice(0, 3)
230
+ .map((c) => (c.commit ? `${c.fecha} \`${c.commit}\`` : c.fecha))
231
+ .join(', ');
232
+ return `- **${modulo}**: ${n} regresi${n === 1 ? 'ón' : 'ones'}${cuando ? ` (${cuando})` : ''}`;
233
+ });
234
+ }
235
+ export function buildSection(rows, changes, alerts = [], projectName, pendingTasks = [], atRiskHealth = [], historial = [], commitPorAnalisis = new Map()) {
150
236
  // Newest-first rows: the first occurrence of a module is its latest state.
151
237
  const seen = new Map();
152
238
  for (const row of rows) {
@@ -210,7 +296,7 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
210
296
  // disparador abstracto: hay que nombrar sus verbos.
211
297
  '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).',
212
298
  '',
213
- 'Tools diferidas, en UNA llamada: ToolSearch `select:mcp__changebook__atlas_project_brief,mcp__changebook__atlas_file_context,mcp__changebook__atlas_symbol_lookup`.',
299
+ 'Tools diferidas, en UNA llamada: ToolSearch `select:mcp__changebook__atlas_project_brief,mcp__changebook__atlas_file_context`.',
214
300
  '',
215
301
  // Proyecto + conducta en UNA línea: cada char de cabecera expulsa mapa.
216
302
  //
@@ -334,7 +420,18 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
334
420
  title: '### Encargos pendientes del dueño (proponte atacarlos)',
335
421
  lines: taskLines,
336
422
  },
337
- { key: 'modules', priority: 3, title: '### Módulos', lines: moduleLines },
423
+ {
424
+ key: 'costoso',
425
+ priority: 1,
426
+ title: '### Lo que ya costó caro aquí (revisa antes de tocarlo)',
427
+ lines: hechosCaros(historial, commitPorAnalisis),
428
+ },
429
+ // El mapa baja de 3 a 4, y no es una degradación caprichosa: `/doctor` de
430
+ // Claude Code recorta por su cuenta «architecture overviews» de un
431
+ // CLAUDE.md porque el agente los deriva del repo. Lo que no puede derivar
432
+ // es qué ha roto antes. Se conserva la sección —quitarla del todo cambia
433
+ // más de lo necesario— pero deja de competir con los hechos.
434
+ { key: 'modules', priority: 4, title: '### Módulos', lines: moduleLines },
338
435
  {
339
436
  key: 'changes',
340
437
  priority: 5,
@@ -359,8 +456,17 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
359
456
  // reserva lo que cuestan las primeras MIN_LINEAS_MAPA líneas del mapa, y solo
360
457
  // el resto se disputa por prioridad. Si el mapa tiene menos líneas que el
361
458
  // suelo, sobra menos reserva; si no hay mapa, no se reserva nada.
362
- const MIN_LINEAS_MAPA = 6;
363
- const mapa = sections.find((x) => x.key === 'modules');
459
+ // EL SUELO CAMBIA DE DUEÑO. Lo tenía el mapa porque el estudio de 76 agentes
460
+ // del 2026-07-20 concluyó que un mapa expulsado era «la causa 1 de que el
461
+ // agente ignore o desconfíe del atlas». Sigue siendo cierto, pero el mapa ya
462
+ // no es lo irreemplazable: el agente lo deriva leyendo el repo, y la propia
463
+ // herramienta lo recorta. Lo que nadie puede derivar es qué rompió antes, así
464
+ // que el suelo protege ahora eso.
465
+ //
466
+ // Y baja de 6 líneas a 3: son entradas mucho más densas —un módulo, un
467
+ // número y hasta tres commits— así que tres ya dicen dónde pisar con cuidado.
468
+ const MIN_LINEAS_MAPA = 3;
469
+ const mapa = sections.find((x) => x.key === 'costoso');
364
470
  let reservaMapa = 0;
365
471
  if (mapa && mapa.lines.length > 0) {
366
472
  reservaMapa = mapa.title.length + 2;
@@ -380,7 +486,7 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
380
486
  if (s.lines.length === 0)
381
487
  continue;
382
488
  // El mapa gasta su reserva ADEMÁS de lo que haya quedado libre.
383
- const disponible = s.key === 'modules' ? budget + reservaMapa : budget;
489
+ const disponible = s.key === 'costoso' ? budget + reservaMapa : budget;
384
490
  let cost = s.title.length + 2; // título + línea en blanco separadora
385
491
  let count = 0;
386
492
  for (const line of s.lines) {
@@ -391,8 +497,14 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
391
497
  }
392
498
  if (count > 0) {
393
499
  includedCount.set(s.key, count);
394
- if (s.key === 'modules') {
500
+ if (s.key === 'costoso') {
395
501
  // Lo que consumió por encima de su reserva sale del fondo común.
502
+ //
503
+ // La clave tiene que ser la MISMA que la de arriba: al mover el suelo
504
+ // del mapa a los hechos se me quedó aquí `modules`, así que la sección
505
+ // nueva gastaba la reserva Y descontaba su coste entero del fondo —
506
+ // doble gasto— y el mapa se llevaba luego una reserva ya usada. El
507
+ // bloque no reventaba el techo por casualidad, no por diseño.
396
508
  budget -= Math.max(0, cost - reservaMapa);
397
509
  reservaMapa = 0;
398
510
  }
package/dist/tools.js CHANGED
@@ -281,76 +281,6 @@ export async function derivaContraHead(dir, hash) {
281
281
  }
282
282
  // ── Registration ──────────────────────────────────────────────────────────────
283
283
  export function registerTools(server, db) {
284
- // Espejo del hospedado (T3 del benchmark 2026-07-20): definiciones
285
- // exportadas por archivo, sin grep. Usos y tests siguen fuera y se dice.
286
- server.registerTool("atlas_symbol_lookup", {
287
- title: "Where is this symbol defined?",
288
- description: `Files where an exported symbol (function/class/const/interface/type/enum) is DEFINED, with its kind and the commit that last touched it. Mechanical index built from every ingested diff.
289
-
290
- Definitions only — usages and tests are not indexed; grep for those. Exact match first, substring fallback.
291
-
292
- Args:
293
- - symbol (required): the identifier to look up.
294
- - project (recommended): the repo you are working in (folder name or slug).
295
-
296
- Returns (structured): { symbol, exact_match, matches: [{ symbol, file, kind, commit }] }`,
297
- inputSchema: {
298
- symbol: z
299
- .string()
300
- .min(2)
301
- .max(120)
302
- .describe("Identifier to look up (exported definition)"),
303
- project: z
304
- .string()
305
- .min(1)
306
- .max(120)
307
- .describe("Project to scope to (repo folder name or slug)"),
308
- },
309
- annotations: {
310
- readOnlyHint: true,
311
- destructiveHint: false,
312
- idempotentHint: true,
313
- openWorldHint: true,
314
- },
315
- }, async ({ symbol, project }) => {
316
- try {
317
- const t0 = Date.now();
318
- const pf = await db.projectFilterFor(project);
319
- const base = `symbol_index?select=symbol,file,kind,commit_hash&order=symbol.asc&limit=20` +
320
- pf;
321
- let matches = await db.rest(`${base}&symbol=eq.${encodeURIComponent(symbol)}`);
322
- let exact = true;
323
- if (matches.length === 0) {
324
- exact = false;
325
- matches = await db.rest(`${base}&symbol=ilike.${ilikePattern(symbol)}`);
326
- }
327
- const lines = [`# Symbol lookup: ${symbol}`, ""];
328
- if (matches.length === 0) {
329
- lines.push("Not in the index. It may be unexported, renamed, or defined before the index existed — fall back to grep and SAY you did, instead of guessing.");
330
- }
331
- for (const m of matches) {
332
- lines.push(`- ${m.symbol} (${m.kind}) — ${m.file}${m.commit_hash ? ` · commit ${m.commit_hash.slice(0, 7)}` : ""}`);
333
- }
334
- if (matches.length > 0) {
335
- lines.push("", "Definitions only — usages and tests are not indexed; grep for those.");
336
- }
337
- const salida = toolResult(lines.join("\n"), {
338
- symbol,
339
- exact_match: exact,
340
- matches: matches.map((m) => ({
341
- symbol: m.symbol,
342
- file: m.file,
343
- kind: m.kind,
344
- commit: m.commit_hash?.slice(0, 7) ?? null,
345
- })),
346
- });
347
- recordRead(db, "atlas_symbol_lookup", pf, servedCharsOf(salida), Date.now() - t0);
348
- return salida;
349
- }
350
- catch (error) {
351
- return errorResult(error);
352
- }
353
- });
354
284
  server.registerTool("atlas_recent_changes", {
355
285
  title: "Recent ChangeBook changes",
356
286
  description: `List the most recent analyzed code changes from the ChangeBook changelog (newest first).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "changebook",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "mcpName": "io.github.raulbr90/changebook",
5
5
  "description": "ChangeBook for coding agents: MCP server (product memory for Claude Code/Codex) + CLI to sign in, analyze changes and sync the product map.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.raulbr90/changebook",
4
4
  "description": "Query your product's living memory: module map + analyzed change history. Read-only MCP tools.",
5
- "version": "0.5.0",
5
+ "version": "0.6.0",
6
6
  "websiteUrl": "https://changebook.dev",
7
7
  "remotes": [
8
8
  {
@@ -15,7 +15,7 @@
15
15
  "registryType": "npm",
16
16
  "registryBaseUrl": "https://registry.npmjs.org",
17
17
  "identifier": "changebook",
18
- "version": "0.5.0",
18
+ "version": "0.6.0",
19
19
  "transport": {
20
20
  "type": "stdio"
21
21
  }