changebook 0.4.4 → 0.4.5
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/guard.js +5 -1
- package/dist/index.js +17 -1
- package/dist/sync.js +34 -11
- package/dist/tools.js +55 -12
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/guard.js
CHANGED
|
@@ -181,7 +181,11 @@ export function contarEnRepo(dir, simbolo) {
|
|
|
181
181
|
// aviso de tipo "esto sigue usandose" encontraria su propia cita y no
|
|
182
182
|
// podria refutarse jamas. Lo cazo el test, no el diseno.
|
|
183
183
|
":!*.md",
|
|
184
|
-
"
|
|
184
|
+
// Con comodín a propósito: un pathspec sin comodín ("docs/") hace
|
|
185
|
+
// abortar a git grep si la carpeta no existe — en cualquier repo de
|
|
186
|
+
// usuario sin docs/ el buscador devolvía null y la refutación moría
|
|
187
|
+
// en silencio. Lo cazó el fixture hermético del test (2026-07-21).
|
|
188
|
+
":!docs/**",
|
|
185
189
|
], { cwd: dir, encoding: "utf8", timeout: 2_000, maxBuffer: 4 * 1024 * 1024 });
|
|
186
190
|
// Una linea "fichero:N" por fichero con coincidencias.
|
|
187
191
|
return out
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,23 @@ async function main() {
|
|
|
131
131
|
case "analyze": {
|
|
132
132
|
const db = new Supabase();
|
|
133
133
|
requireCredentials(db);
|
|
134
|
-
|
|
134
|
+
const options = parseAnalyzeArgs(process.argv.slice(3));
|
|
135
|
+
await analyze(db, options);
|
|
136
|
+
// El mapa de CLAUDE.md/AGENTS.md se regeneraba solo en `init`/`sync`,
|
|
137
|
+
// así que se congelaba el día que lo instalabas (estudio 2026-07-20: la
|
|
138
|
+
// causa nº 1 de que el agente desconfíe del atlas). analyze es el camino
|
|
139
|
+
// del hook post-commit, o sea CADA commit: refrescar aquí hace el mapa
|
|
140
|
+
// fresco por construcción, como watched_values. refreshOnly: jamás crea
|
|
141
|
+
// archivos ni resucita un bloque borrado. Best-effort: un sync caído no
|
|
142
|
+
// puede tumbar un análisis ya cobrado y registrado.
|
|
143
|
+
try {
|
|
144
|
+
await syncContextFiles(db, options.dir ?? process.cwd(), {
|
|
145
|
+
refreshOnly: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error(`Map refresh failed (analysis itself succeeded): ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
}
|
|
135
151
|
return;
|
|
136
152
|
}
|
|
137
153
|
case "import": {
|
package/dist/sync.js
CHANGED
|
@@ -53,7 +53,7 @@ const MIN_PAIR_RATE = 0.6;
|
|
|
53
53
|
// Same window/limit the web uses for the signals strip.
|
|
54
54
|
const ALERT_WINDOW_DAYS = 14;
|
|
55
55
|
const MAX_ALERTS = 3;
|
|
56
|
-
export async function syncContextFiles(db, targetDir) {
|
|
56
|
+
export async function syncContextFiles(db, targetDir, opts = {}) {
|
|
57
57
|
const projectName = projectNameFor(targetDir);
|
|
58
58
|
// Frontera por proyecto también aquí (QA 2026-07-18): sin filtro, una
|
|
59
59
|
// cuenta con varios proyectos construiría el mapa de ESTE repo mezclando
|
|
@@ -103,7 +103,7 @@ export async function syncContextFiles(db, targetDir) {
|
|
|
103
103
|
const section = buildSection(moduleRows, changes, alerts, projectName, pendingTasks);
|
|
104
104
|
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
105
105
|
const file = path.join(targetDir, name);
|
|
106
|
-
const updated = await upsertSection(file, section);
|
|
106
|
+
const updated = await upsertSection(file, section, opts);
|
|
107
107
|
console.error(`${updated} ${name}`);
|
|
108
108
|
}
|
|
109
109
|
// El sync ES una consulta del atlas — la más apalancada: el mapa que
|
|
@@ -159,15 +159,28 @@ export function buildSection(rows, changes, alerts = [], projectName, pendingTas
|
|
|
159
159
|
// · cargar las tools diferidas de UNA vez (benchmark 2026-07-20: 3 de 7
|
|
160
160
|
// llamadas del agente CON atlas eran ToolSearch cargando esquemas de
|
|
161
161
|
// uno en uno — cada una relee ~45k tokens de contexto)
|
|
162
|
-
|
|
162
|
+
//
|
|
163
|
+
// La FORMA de las instrucciones es disparador → tool, no prosa. Medido en
|
|
164
|
+
// la tanda 8808ebe (adopcion por celda, 2026-07-21): la adopcion sigue al
|
|
165
|
+
// matching LITERAL entre la tarea y el disparador escrito — "vas a editar
|
|
166
|
+
// el archivo" casaba con el disparador de file_context y adopto 5/5; el
|
|
167
|
+
// generico de orientacion 3/5; "investiga la funcion X", sin disparador,
|
|
168
|
+
// 0/5. Un agente no infiere que una tool aplica: reconoce su tarea en el
|
|
169
|
+
// texto o no la usa. Por eso cada tool lleva delante la formulacion de
|
|
170
|
+
// tarea que debe capturar, y el ToolSearch va con el select completo
|
|
171
|
+
// listo para copiar (cargar esquemas de uno en uno costaba 3 turnos).
|
|
172
|
+
// El disparador de symbol_lookup se RETIRÓ el 2026-07-21 con medición en
|
|
173
|
+
// la mano (tanda 957372c): llevó la adopción de t3 de 0/5 a 3/5, pero los
|
|
174
|
+
// adoptantes gastaron 607k tok/9 turnos vs 254k/4 del brazo sin atlas —
|
|
175
|
+
// la tool solo cubre definiciones y el agente acababa pagando atlas Y
|
|
176
|
+
// grep. Con repo local, grep gana; el sitio de symbol_lookup es el agente
|
|
177
|
+
// hospedado sin repo. Medir → actuar, aunque sea des-haciendo lo de ayer.
|
|
178
|
+
'Disparador → tool: orientarte → `atlas_project_brief` (`intent:"orient"`) · editar un archivo → `atlas_file_context` con su ruta · riesgos/acoplamientos → el brief. Tras cada commit: `atlas_record_change` (diff, hash, fecha, `summary` tuyo; reenviar es gratis).',
|
|
179
|
+
'',
|
|
180
|
+
'Tools diferidas, en UNA llamada: ToolSearch `select:mcp__changebook__atlas_project_brief,mcp__changebook__atlas_file_context,mcp__changebook__atlas_symbol_lookup`.',
|
|
163
181
|
'',
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`Pasa SIEMPRE \`project: "${sanitizeCell(projectName)}"\`: el atlas es por proyecto y los datos de otros no son de esta sesión.`,
|
|
167
|
-
'',
|
|
168
|
-
]
|
|
169
|
-
: []),
|
|
170
|
-
'Habla: anuncia en 2-3 líneas qué vas a hacer antes de atacar un encargo, y cuéntale al usuario cualquier riesgo que el atlas te enseñe — él no ve esos avisos.',
|
|
182
|
+
// Proyecto + conducta en UNA línea: cada char de cabecera expulsa mapa.
|
|
183
|
+
`${projectName ? `Pasa SIEMPRE \`project: "${sanitizeCell(projectName)}"\`. ` : ''}Anuncia antes de atacar un encargo, y DILE al usuario los riesgos que el atlas te enseñe — él no los ve.`,
|
|
171
184
|
'',
|
|
172
185
|
];
|
|
173
186
|
if (modules.length === 0) {
|
|
@@ -345,7 +358,7 @@ export function coChangePairs(rows) {
|
|
|
345
358
|
return pairs.sort((x, y) => y.rate - x.rate).slice(0, MAX_COUPLINGS);
|
|
346
359
|
}
|
|
347
360
|
/** Exported for tests. */
|
|
348
|
-
export async function upsertSection(file, section) {
|
|
361
|
+
export async function upsertSection(file, section, opts = {}) {
|
|
349
362
|
let content = null;
|
|
350
363
|
try {
|
|
351
364
|
content = await readFile(file, 'utf8');
|
|
@@ -354,6 +367,8 @@ export async function upsertSection(file, section) {
|
|
|
354
367
|
content = null;
|
|
355
368
|
}
|
|
356
369
|
if (content === null) {
|
|
370
|
+
if (opts.refreshOnly)
|
|
371
|
+
return 'skipped';
|
|
357
372
|
await writeFile(file, section + '\n');
|
|
358
373
|
return 'created';
|
|
359
374
|
}
|
|
@@ -362,6 +377,14 @@ export async function upsertSection(file, section) {
|
|
|
362
377
|
// nuevo — si no, quedarían dos mapas (uno obsoleto) en el mismo archivo.
|
|
363
378
|
const LEGACY_START = '<!-- appatlas:start -->';
|
|
364
379
|
const LEGACY_END = '<!-- appatlas:end -->';
|
|
380
|
+
// Un archivo sin NINGÚN bloque (ni actual ni legado) en modo refreshOnly es
|
|
381
|
+
// una decisión del dueño, no un hueco que rellenar: borró el mapa y el
|
|
382
|
+
// refresco automático no puede volver a pegárselo en cada commit.
|
|
383
|
+
if (opts.refreshOnly &&
|
|
384
|
+
!(content.includes(START) && content.includes(END)) &&
|
|
385
|
+
!(content.includes(LEGACY_START) && content.includes(LEGACY_END))) {
|
|
386
|
+
return 'skipped';
|
|
387
|
+
}
|
|
365
388
|
const legacyStart = content.indexOf(LEGACY_START);
|
|
366
389
|
const legacyEnd = content.indexOf(LEGACY_END);
|
|
367
390
|
if (legacyStart !== -1 && legacyEnd !== -1 && legacyEnd > legacyStart) {
|
package/dist/tools.js
CHANGED
|
@@ -121,6 +121,23 @@ export function filesUnionByChange(rows, cap = FILES_CAP) {
|
|
|
121
121
|
}
|
|
122
122
|
return out;
|
|
123
123
|
}
|
|
124
|
+
// Espejo de supabase/functions/mcp/scope.ts (paridad en
|
|
125
|
+
// test/espejoDeCommits.test.ts): aliases post-squash del mismo contenido,
|
|
126
|
+
// para que el hash citado exista en el main del consultante.
|
|
127
|
+
export function commitAliasesShort(raw) {
|
|
128
|
+
if (!Array.isArray(raw))
|
|
129
|
+
return [];
|
|
130
|
+
return raw
|
|
131
|
+
.filter((h) => typeof h === "string" && /^[0-9a-f]{7,64}$/i.test(h))
|
|
132
|
+
.map((h) => h.slice(0, 7));
|
|
133
|
+
}
|
|
134
|
+
export function commitLabel(hash, aliasesRaw) {
|
|
135
|
+
const corto = hash?.slice(0, 7) ?? null;
|
|
136
|
+
if (!corto)
|
|
137
|
+
return null;
|
|
138
|
+
const aliases = commitAliasesShort(aliasesRaw);
|
|
139
|
+
return aliases.length > 0 ? `${corto} (=${aliases.join(",")})` : corto;
|
|
140
|
+
}
|
|
124
141
|
// Consultation metering (never billing): each successful read leaves a row in
|
|
125
142
|
// atlas_reads so the web can show "your agent consulted the atlas N times".
|
|
126
143
|
// Best-effort and non-blocking — metering must never break or slow a read.
|
|
@@ -294,7 +311,7 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
294
311
|
try {
|
|
295
312
|
const t0 = Date.now();
|
|
296
313
|
const pf = await db.projectFilterFor(project);
|
|
297
|
-
let query = `changelog?select=id,business_impact,summary_tech,created_at,diff_character_count,commit_hash` +
|
|
314
|
+
let query = `changelog?select=id,business_impact,summary_tech,created_at,diff_character_count,commit_hash,hash_aliases` +
|
|
298
315
|
`&order=created_at.desc&limit=${limit}&offset=${offset}` +
|
|
299
316
|
pf;
|
|
300
317
|
if (search) {
|
|
@@ -332,6 +349,9 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
332
349
|
...(include_tech || search ? { summary_tech: r.summary_tech ?? null } : {}),
|
|
333
350
|
diff_chars: r.diff_character_count ?? null,
|
|
334
351
|
commit: r.commit_hash?.slice(0, 7) ?? null,
|
|
352
|
+
// Post-squash: el mismo cambio bajo otro hash (rama vs main). Si
|
|
353
|
+
// `commit` no existe en tu repo, uno de estos sí.
|
|
354
|
+
commit_aliases: commitAliasesShort(r.hash_aliases),
|
|
335
355
|
files: filesByChange.get(r.id)?.files ?? [],
|
|
336
356
|
files_more: filesByChange.get(r.id)?.more ?? 0,
|
|
337
357
|
modules: (modulesByChange.get(r.id) ?? []).map((m) => ({
|
|
@@ -350,7 +370,7 @@ Don't use for per-module deep dives — use atlas_module_detail for that.`,
|
|
|
350
370
|
const mods = c.modules
|
|
351
371
|
.map((m) => m.module + (m.risk ? ` [${m.risk}]` : ""))
|
|
352
372
|
.join(", ");
|
|
353
|
-
lines.push(`## ${c.date}${c.commit ? ` · ${c.commit}` : ""} — ${c.business_impact}`);
|
|
373
|
+
lines.push(`## ${c.date}${c.commit ? ` · ${c.commit}${c.commit_aliases.length > 0 ? ` (=${c.commit_aliases.join(",")})` : ""}` : ""} — ${c.business_impact}`);
|
|
354
374
|
if ((include_tech || search) && c.summary_tech)
|
|
355
375
|
lines.push(`- Tech: ${c.summary_tech}`);
|
|
356
376
|
if (mods)
|
|
@@ -499,7 +519,7 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
499
519
|
};
|
|
500
520
|
}
|
|
501
521
|
const ids = [...new Set(rows.map((r) => r.changelog_id))].join(",");
|
|
502
|
-
const logs = await db.rest(`changelog?select=id,business_impact,summary_tech,created_at,diff_character_count,commit_hash&id=in.(${ids})`);
|
|
522
|
+
const logs = await db.rest(`changelog?select=id,business_impact,summary_tech,created_at,diff_character_count,commit_hash,hash_aliases&id=in.(${ids})`);
|
|
503
523
|
const logById = new Map(logs.map((l) => [l.id, l]));
|
|
504
524
|
const changes = rows.map((r) => {
|
|
505
525
|
let excerpt = null;
|
|
@@ -519,6 +539,7 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
519
539
|
// El commit del que salió cada entrada: la nota deja de flotar en
|
|
520
540
|
// el tiempo y se puede cuadrar contra git.
|
|
521
541
|
commit: logById.get(r.changelog_id)?.commit_hash?.slice(0, 7) ?? null,
|
|
542
|
+
commit_aliases: commitAliasesShort(logById.get(r.changelog_id)?.hash_aliases),
|
|
522
543
|
risk: r.risk,
|
|
523
544
|
note: r.note,
|
|
524
545
|
tech: r.tech,
|
|
@@ -530,12 +551,24 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
530
551
|
});
|
|
531
552
|
// Ancla temporal de la respuesta entera: el commit más nuevo servido,
|
|
532
553
|
// comparado con el HEAD del árbol — solo si este árbol ES el proyecto
|
|
533
|
-
// (misma puerta que la refutación de alertas).
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
554
|
+
// (misma puerta que la refutación de alertas). Post-squash, el hash
|
|
555
|
+
// primario puede no existir en este árbol: se prueban sus aliases
|
|
556
|
+
// antes de rendirse.
|
|
557
|
+
let ancla = null;
|
|
558
|
+
if (cwdEsElProyecto(project)) {
|
|
559
|
+
const log = rows
|
|
560
|
+
.map((r) => logById.get(r.changelog_id))
|
|
561
|
+
.find((l) => l?.commit_hash);
|
|
562
|
+
const candidatos = [
|
|
563
|
+
log?.commit_hash ?? null,
|
|
564
|
+
...commitAliasesShort(log?.hash_aliases),
|
|
565
|
+
];
|
|
566
|
+
for (const c of candidatos) {
|
|
567
|
+
ancla = await derivaContraHead(process.cwd(), c);
|
|
568
|
+
if (ancla)
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
539
572
|
const latest = rows[0];
|
|
540
573
|
const output = {
|
|
541
574
|
module,
|
|
@@ -553,7 +586,7 @@ Returns (structured): { module, count, changes: [{ date, commit, risk, note, tec
|
|
|
553
586
|
lines.push(ancla);
|
|
554
587
|
lines.push("");
|
|
555
588
|
for (const c of changes) {
|
|
556
|
-
lines.push(`## ${c.date}${c.commit ? ` · commit ${c.commit}` : ""}${c.risk ? ` — risk: ${c.risk}` : ""}`);
|
|
589
|
+
lines.push(`## ${c.date}${c.commit ? ` · commit ${c.commit}${c.commit_aliases.length > 0 ? ` (=${c.commit_aliases.join(",")})` : ""}` : ""}${c.risk ? ` — risk: ${c.risk}` : ""}`);
|
|
557
590
|
if (c.business_impact)
|
|
558
591
|
lines.push(`- Impact: ${c.business_impact}`);
|
|
559
592
|
if (c.note)
|
|
@@ -678,10 +711,20 @@ Returns (structured): { files: [{ file, modules: [{ module, risk, changes, last_
|
|
|
678
711
|
let ancla = null;
|
|
679
712
|
if (cwdEsElProyecto(project)) {
|
|
680
713
|
const ultimo = await db
|
|
681
|
-
.rest(`changelog?select=commit_hash&commit_hash=not.is.null&order=created_at.desc&limit=1` +
|
|
714
|
+
.rest(`changelog?select=commit_hash,hash_aliases&commit_hash=not.is.null&order=created_at.desc&limit=1` +
|
|
682
715
|
pf)
|
|
683
716
|
.catch(() => []);
|
|
684
|
-
|
|
717
|
+
// Post-squash: si el hash primario no existe en este árbol (era el
|
|
718
|
+
// de la rama), sus aliases sí pueden — probar antes de rendirse.
|
|
719
|
+
const candidatos = [
|
|
720
|
+
ultimo[0]?.commit_hash ?? null,
|
|
721
|
+
...commitAliasesShort(ultimo[0]?.hash_aliases),
|
|
722
|
+
];
|
|
723
|
+
for (const c of candidatos) {
|
|
724
|
+
ancla = await derivaContraHead(process.cwd(), c);
|
|
725
|
+
if (ancla)
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
685
728
|
}
|
|
686
729
|
const lines = [`# File context (${paths.length} file(s))`];
|
|
687
730
|
if (ancla)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "changebook",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"start": "node dist/index.js",
|
|
32
|
-
"build": "tsc",
|
|
32
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"clean": "rm -rf dist",
|
|
35
35
|
"prepublishOnly": "npm run build"
|
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.4.
|
|
5
|
+
"version": "0.4.5",
|
|
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.4.
|
|
18
|
+
"version": "0.4.5",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
}
|