composto-ai 0.7.1 → 0.8.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/README.md CHANGED
@@ -1,23 +1,23 @@
1
1
  # Composto
2
2
 
3
- **Causal memory layer for coding agents. Catches the bug your agent is about to reintroduce.**
3
+ **Token-efficient code context for AI agents. Your file's full structure in a fraction of the tokens, with its causal history baked in.**
4
4
 
5
- Composto is a repo-local graph of your git history that your AI coding agent consults before every edit. When a file was reverted recently, has a fix cluster in its history, or was last touched by someone who left the team, Composto surfaces that signal as in-context guidance before the agent writes the code. Hook-enforced on Claude Code, Cursor, and Gemini CLI. Local-first, MIT.
5
+ Composto compresses any source file into a Health-Aware IR that keeps exactly what your agent needs signatures, types, control flow, dependencies at 60-95% fewer tokens than raw code. On top of that, it surfaces the file's causal history (what historically changed and broke alongside the code you're touching) as advisory context. Local-first, MIT. Works with Claude Code, Cursor, and Gemini CLI.
6
6
 
7
7
  ```
8
- $ composto impact src/memory/signals/hotspot.ts
9
-
10
- verdict: medium
11
- score: 0.52
12
- confidence: 0.50
13
- signals:
14
- revert_match ■■■■■■■■■■ strength=1.00 precision=1.00
15
- hotspot ■ strength=0.10 precision=0.54
16
- fix_ratio ■ strength=0.07 precision=0.54
17
- author_churn · strength=0.00 precision=0.16
18
-
19
- # This file was touched by a Revert commit in history.
20
- # blastradius remembers. Your LLM couldn't.
8
+ $ composto ir src/memory/confidence.ts L1
9
+
10
+ USE:./types.js
11
+ OUT INTERFACE:ConfidenceContext
12
+ OUT INTERFACE:ScoreAndConfidence
13
+ FN:calibrationFactor(signals: Signal[])
14
+ GUARD:[firing.length === 0 → 1.0, avg < 20 → 0.3, avg < 100 → 0.6]
15
+ FN:historyFactor(totalCommits: number)
16
+ GUARD:[totalCommits < 50 → 0.2, totalCommits < 200 → 0.5, totalCommits < 1000 → 0.8]
17
+ OUT FN:computeScoreAndConfidence(signals: Signal[], ctx: ConfidenceContext)
18
+
19
+ # 541 tokens of raw code 230 tokens of IR (57% fewer). Structure intact:
20
+ # every signature, dependency, and decision threshold survives.
21
21
  ```
22
22
 
23
23
  ---
@@ -45,9 +45,9 @@ composto impact src/auth/login.ts
45
45
  composto index --status # diagnostics: schema, freshness, calibration
46
46
  ```
47
47
 
48
- ### Also in the box: AST compression tools
48
+ ### The core: token-efficient structural context
49
49
 
50
- Composto also ships a tree-sitter based AST compressor (about 89% token savings) and a smart context packer for bug-fix tasks. These are separate from the causal layer but live in the same binary.
50
+ Composto's spine is a tree-sitter based AST compressor and a smart context packer. Compress any file to IR, or pack a whole directory into a token budget:
51
51
 
52
52
  ```bash
53
53
  composto ir src/app.ts # compress a file to IR (L0/L1/L2/L3)
@@ -57,6 +57,10 @@ composto benchmark . # see compression stats
57
57
 
58
58
  See the [IR Layers](#ir-layers), [Health-Aware IR](#health-aware-ir), and [Context Budget](#context-budget) sections below for details.
59
59
 
60
+ ### On top: causal history as advisory context
61
+
62
+ Composto also indexes your git history and surfaces what historically changed and broke alongside the file you're editing — advisory context the agent weighs, not a gate. See [Causal context](#causal-context) below.
63
+
60
64
  ### MCP plugin (Claude Code, Cursor, Claude Desktop)
61
65
 
62
66
  The MCP server is bundled inside `composto-ai`. Install the package globally first, then register the server with your client:
@@ -188,27 +192,25 @@ composto index --status # diagnostics: schema, freshness, calibration
188
192
 
189
193
  ---
190
194
 
191
- ## BlastRadius
195
+ ## Causal context
192
196
 
193
- Beyond compression, Composto indexes your repo's git history into a local SQLite graph and exposes it as a queryable risk surface. Before your agent edits a file, it can ask: *"has this region been reverted? who fixed the last similar bug? is the last author still around?"* — signals no LLM can infer from current code alone.
197
+ On top of compression, Composto indexes your repo's git history into a local SQLite graph and surfaces what the current code can't tell you: *"has this region been reverted? does it have a fix cluster? what files historically changed and broke alongside it?"* — context no LLM can infer from the file alone. It's delivered as **advisory context the agent weighs**, not a hard gate.
194
198
 
195
- Four signals per query: `revert_match`, `hotspot`, `fix_ratio`, `author_churn`. Verdict is `low` / `medium` / `high` / `unknown`; when confidence is low the tool returns `unknown` rather than guessing. Precision is repo-calibrated (self-validation over the repo's own fix history).
199
+ Signals per query: `revert_match`, `hotspot`, `fix_ratio`, `author_churn`, `cochange`. The tool returns `unknown` when confidence is low rather than guessing.
196
200
 
197
201
  ```
198
- verdict: high
199
- score: 1.00
200
- confidence: 0.30
201
- signals:
202
- revert_match ■■■■■■■■■■ strength=1.00 precision=0.50
203
- hotspot · strength=0.00 precision=0.30
204
- ...
202
+ $ composto impact src/auth/login.ts
203
+
204
+ revert_match ■■■■■■■■■■ this file was touched by a Revert commit
205
+ cochange ■■■■■ historically co-changed with session.ts, token.ts in fixes
206
+ hotspot ■ 14 changes in the last 90 days
205
207
  ```
206
208
 
207
- **Where we are, honestly.** The v2.1 time-travel backtest (rewinds the DB to each pre-fix snapshot) shows `revert_match` carrying most of the product's value it clears the ship gate on picomatch (precision 0.65, recall 0.78 on the `medium|high` band). Signal-attributed precision (excluding `revert_match`) is weaker: the three non-revert signals are alive but need calibration work. See [docs/blastradius-proof-v2.md](docs/blastradius-proof-v2.md) for numbers on all four band combinations across two public repos + the per-signal diagnostic behind them.
209
+ **Where we are, honestly.** A 4-repo time-travel backtest (fastify, express, got, flask each rewound to pre-fix snapshots) shows the causal layer is a **high-recall, advisory-grade** signal: on mature repos it recovers 67-80% of the files a fix actually touches. Precision is modest (~0.55) these signals point you at *candidates*, they don't certify them, which is exactly why Composto surfaces them as context for the agent to judge rather than as a blocking verdict. Recall scales with git history, so the value grows as your repo matures (a young repo gets little until it accumulates fix history).
208
210
 
209
- The honest framing: **BlastRadius is a bug-history memory layer that agents query before editing.** "This file was reverted three weeks ago" is the primary promise v1 delivers. The other signals expand the query surface calibration work on `hotspot` and `fix_ratio` is the open follow-on.
211
+ The honest framing: **causal context is a high-recall memory layer agents consult before editing** "these files have a history of breaking together" not a precision gate. The compression core works unconditionally; the causal layer adds repo-specific memory on top.
210
212
 
211
- Feature-flagged via `COMPOSTO_BLASTRADIUS=1` during the beta. Available as both CLI (`composto impact`, `composto index`) and MCP tool (`composto_blastradius`).
213
+ Available as CLI (`composto impact`, `composto index`) and MCP tool (`composto_blastradius`, gated by `COMPOSTO_BLASTRADIUS=1` during beta).
212
214
 
213
215
  ---
214
216
 
@@ -307,14 +309,14 @@ Hotspot files get full detail. Everything else gets structure. Budget is never e
307
309
  ## Stats
308
310
 
309
311
  ```
310
- Overall compression: 89.2%
311
- L0 compression: 97.5%
312
- AST engine: 51/51 files (0 regex fallback)
312
+ L1 compression: ~81% fewer tokens (full IR, structure preserved)
313
+ L0 compression: ~97% fewer tokens (structure map)
314
+ Token counts: verified against a real BPE tokenizer, not estimates
315
+ AST engine: AST-parsed, 0 regex fallback
313
316
  Languages: TypeScript, JavaScript, Python, Go, Rust
314
- Tests: 251 passing
315
- BlastRadius v2.1: precision 0.65, recall 0.78 (picomatch, time-travel,
316
- medium|high band). Honest signal-attributed numbers
317
- in docs/blastradius-proof-v2.md.
317
+ Causal layer: high-recall advisory (0.67-0.80 recall on mature repos,
318
+ time-travel backtest across 4 public repos); precision
319
+ ~0.55, surfaced as context not a gate.
318
320
  ```
319
321
 
320
322
  ---
package/dist/index.js CHANGED
@@ -2125,13 +2125,38 @@ function computeAuthorChurn(db, filePath) {
2125
2125
  return { ...base, strength };
2126
2126
  }
2127
2127
 
2128
+ // src/memory/signals/cochange.ts
2129
+ var SATURATION_DEGREE = 10;
2130
+ var FALLBACK_PRECISION5 = 0.3;
2131
+ function computeCochange(db, filePath) {
2132
+ const row = db.prepare(`
2133
+ SELECT COUNT(DISTINCT ft2.file_path) AS degree
2134
+ FROM file_touches ft1
2135
+ JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
2136
+ JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
2137
+ WHERE ft1.file_path = ?
2138
+ `).get(filePath);
2139
+ const degree = row?.degree ?? 0;
2140
+ const strength = Math.min(1, degree / SATURATION_DEGREE);
2141
+ const cal = getCalibration(db, "cochange", FALLBACK_PRECISION5);
2142
+ return {
2143
+ type: "cochange",
2144
+ strength,
2145
+ precision: cal.precision,
2146
+ sample_size: cal.sampleSize,
2147
+ evidence: [],
2148
+ cochange_degree: degree
2149
+ };
2150
+ }
2151
+
2128
2152
  // src/memory/signals/index.ts
2129
2153
  function collectSignals(db, _repoPath, filePath) {
2130
2154
  return [
2131
2155
  computeRevertMatch(db, filePath),
2132
2156
  computeHotspot(db, filePath),
2133
2157
  computeFixRatio(db, filePath),
2134
- computeAuthorChurn(db, filePath)
2158
+ computeAuthorChurn(db, filePath),
2159
+ computeCochange(db, filePath)
2135
2160
  ];
2136
2161
  }
2137
2162
 
@@ -2170,18 +2195,29 @@ function historyFactor(totalCommits) {
2170
2195
  if (totalCommits < 1e3) return 0.8;
2171
2196
  return 1;
2172
2197
  }
2198
+ var DEFAULT_COCHANGE_FLOOR = 1;
2199
+ function cochangeFloor() {
2200
+ const v = Number(process.env.COMPOSTO_COCHANGE_FLOOR);
2201
+ return Number.isFinite(v) && v >= 0 && v <= 1 ? v : DEFAULT_COCHANGE_FLOOR;
2202
+ }
2173
2203
  function computeScoreAndConfidence(signals, ctx) {
2204
+ const cochange = signals.find((s) => s.type === "cochange");
2205
+ const averaged = signals.filter((s) => s.type !== "cochange");
2174
2206
  let num = 0;
2175
2207
  let den = 0;
2176
- for (const s of signals) {
2208
+ for (const s of averaged) {
2177
2209
  if (s.strength <= 0 || s.precision <= 0) continue;
2178
2210
  num += s.strength * s.precision;
2179
2211
  den += s.precision;
2180
2212
  }
2181
- const score = den === 0 ? 0 : num / den;
2213
+ let score = den === 0 ? 0 : num / den;
2214
+ if (cochange) {
2215
+ const floor = cochangeFloor();
2216
+ score *= floor + (1 - floor) * cochange.strength;
2217
+ }
2182
2218
  const confidence = Math.min(
2183
- coverageFactor(signals),
2184
- calibrationFactor(signals),
2219
+ coverageFactor(averaged),
2220
+ calibrationFactor(averaged),
2185
2221
  freshnessFactor(ctx),
2186
2222
  historyFactor(ctx.totalCommits)
2187
2223
  );
@@ -1929,13 +1929,38 @@ function computeAuthorChurn(db, filePath) {
1929
1929
  return { ...base, strength };
1930
1930
  }
1931
1931
 
1932
+ // src/memory/signals/cochange.ts
1933
+ var SATURATION_DEGREE = 10;
1934
+ var FALLBACK_PRECISION5 = 0.3;
1935
+ function computeCochange(db, filePath) {
1936
+ const row = db.prepare(`
1937
+ SELECT COUNT(DISTINCT ft2.file_path) AS degree
1938
+ FROM file_touches ft1
1939
+ JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
1940
+ JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
1941
+ WHERE ft1.file_path = ?
1942
+ `).get(filePath);
1943
+ const degree = row?.degree ?? 0;
1944
+ const strength = Math.min(1, degree / SATURATION_DEGREE);
1945
+ const cal = getCalibration(db, "cochange", FALLBACK_PRECISION5);
1946
+ return {
1947
+ type: "cochange",
1948
+ strength,
1949
+ precision: cal.precision,
1950
+ sample_size: cal.sampleSize,
1951
+ evidence: [],
1952
+ cochange_degree: degree
1953
+ };
1954
+ }
1955
+
1932
1956
  // src/memory/signals/index.ts
1933
1957
  function collectSignals(db, _repoPath, filePath) {
1934
1958
  return [
1935
1959
  computeRevertMatch(db, filePath),
1936
1960
  computeHotspot(db, filePath),
1937
1961
  computeFixRatio(db, filePath),
1938
- computeAuthorChurn(db, filePath)
1962
+ computeAuthorChurn(db, filePath),
1963
+ computeCochange(db, filePath)
1939
1964
  ];
1940
1965
  }
1941
1966
 
@@ -1974,18 +1999,29 @@ function historyFactor(totalCommits) {
1974
1999
  if (totalCommits < 1e3) return 0.8;
1975
2000
  return 1;
1976
2001
  }
2002
+ var DEFAULT_COCHANGE_FLOOR = 1;
2003
+ function cochangeFloor() {
2004
+ const v = Number(process.env.COMPOSTO_COCHANGE_FLOOR);
2005
+ return Number.isFinite(v) && v >= 0 && v <= 1 ? v : DEFAULT_COCHANGE_FLOOR;
2006
+ }
1977
2007
  function computeScoreAndConfidence(signals, ctx) {
2008
+ const cochange = signals.find((s) => s.type === "cochange");
2009
+ const averaged = signals.filter((s) => s.type !== "cochange");
1978
2010
  let num = 0;
1979
2011
  let den = 0;
1980
- for (const s of signals) {
2012
+ for (const s of averaged) {
1981
2013
  if (s.strength <= 0 || s.precision <= 0) continue;
1982
2014
  num += s.strength * s.precision;
1983
2015
  den += s.precision;
1984
2016
  }
1985
- const score = den === 0 ? 0 : num / den;
2017
+ let score = den === 0 ? 0 : num / den;
2018
+ if (cochange) {
2019
+ const floor = cochangeFloor();
2020
+ score *= floor + (1 - floor) * cochange.strength;
2021
+ }
1986
2022
  const confidence = Math.min(
1987
- coverageFactor(signals),
1988
- calibrationFactor(signals),
2023
+ coverageFactor(averaged),
2024
+ calibrationFactor(averaged),
1989
2025
  freshnessFactor(ctx),
1990
2026
  historyFactor(ctx.totalCommits)
1991
2027
  );
@@ -364,13 +364,38 @@ function computeAuthorChurn(db, filePath) {
364
364
  return { ...base, strength };
365
365
  }
366
366
 
367
+ // src/memory/signals/cochange.ts
368
+ var SATURATION_DEGREE = 10;
369
+ var FALLBACK_PRECISION5 = 0.3;
370
+ function computeCochange(db, filePath) {
371
+ const row = db.prepare(`
372
+ SELECT COUNT(DISTINCT ft2.file_path) AS degree
373
+ FROM file_touches ft1
374
+ JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
375
+ JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
376
+ WHERE ft1.file_path = ?
377
+ `).get(filePath);
378
+ const degree = row?.degree ?? 0;
379
+ const strength = Math.min(1, degree / SATURATION_DEGREE);
380
+ const cal = getCalibration(db, "cochange", FALLBACK_PRECISION5);
381
+ return {
382
+ type: "cochange",
383
+ strength,
384
+ precision: cal.precision,
385
+ sample_size: cal.sampleSize,
386
+ evidence: [],
387
+ cochange_degree: degree
388
+ };
389
+ }
390
+
367
391
  // src/memory/signals/index.ts
368
392
  function collectSignals(db, _repoPath, filePath) {
369
393
  return [
370
394
  computeRevertMatch(db, filePath),
371
395
  computeHotspot(db, filePath),
372
396
  computeFixRatio(db, filePath),
373
- computeAuthorChurn(db, filePath)
397
+ computeAuthorChurn(db, filePath),
398
+ computeCochange(db, filePath)
374
399
  ];
375
400
  }
376
401
 
@@ -409,18 +434,29 @@ function historyFactor(totalCommits) {
409
434
  if (totalCommits < 1e3) return 0.8;
410
435
  return 1;
411
436
  }
437
+ var DEFAULT_COCHANGE_FLOOR = 1;
438
+ function cochangeFloor() {
439
+ const v = Number(process.env.COMPOSTO_COCHANGE_FLOOR);
440
+ return Number.isFinite(v) && v >= 0 && v <= 1 ? v : DEFAULT_COCHANGE_FLOOR;
441
+ }
412
442
  function computeScoreAndConfidence(signals, ctx) {
443
+ const cochange = signals.find((s) => s.type === "cochange");
444
+ const averaged = signals.filter((s) => s.type !== "cochange");
413
445
  let num = 0;
414
446
  let den = 0;
415
- for (const s of signals) {
447
+ for (const s of averaged) {
416
448
  if (s.strength <= 0 || s.precision <= 0) continue;
417
449
  num += s.strength * s.precision;
418
450
  den += s.precision;
419
451
  }
420
- const score = den === 0 ? 0 : num / den;
452
+ let score = den === 0 ? 0 : num / den;
453
+ if (cochange) {
454
+ const floor = cochangeFloor();
455
+ score *= floor + (1 - floor) * cochange.strength;
456
+ }
421
457
  const confidence = Math.min(
422
- coverageFactor(signals),
423
- calibrationFactor(signals),
458
+ coverageFactor(averaged),
459
+ calibrationFactor(averaged),
424
460
  freshnessFactor(ctx),
425
461
  historyFactor(ctx.totalCommits)
426
462
  );
@@ -283,11 +283,31 @@ function validateAuthorChurn(db) {
283
283
  const hits = db.prepare(`SELECT COUNT(*) AS n FROM fix_links`).get().n;
284
284
  return { total, hits };
285
285
  }
286
+ function validateCochange(db) {
287
+ const total = db.prepare(`
288
+ SELECT COUNT(DISTINCT ft1.file_path) AS n
289
+ FROM file_touches ft1
290
+ JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
291
+ JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
292
+ `).get().n;
293
+ const hits = db.prepare(`
294
+ SELECT COUNT(DISTINCT ft1.file_path) AS n
295
+ FROM file_touches ft1
296
+ JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
297
+ JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
298
+ WHERE ft1.file_path IN (
299
+ SELECT ft.file_path FROM file_touches ft
300
+ JOIN fix_links fl ON fl.suspected_break_sha = ft.commit_sha
301
+ )
302
+ `).get().n;
303
+ return { total, hits };
304
+ }
286
305
  var VALIDATORS = {
287
306
  revert_match: validateRevertMatch,
288
307
  hotspot: validateHotspot,
289
308
  fix_ratio: validateFixRatio,
290
- author_churn: validateAuthorChurn
309
+ author_churn: validateAuthorChurn,
310
+ cochange: validateCochange
291
311
  };
292
312
  function refreshCalibration(db, headSha) {
293
313
  const now = Math.floor(Date.now() / 1e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composto-ai",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {