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 +39 -37
- package/dist/index.js +41 -5
- package/dist/mcp/server.js +41 -5
- package/dist/memory/api.js +41 -5
- package/dist/memory/worker.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# Composto
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
signals:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
-
###
|
|
48
|
+
### The core: token-efficient structural context
|
|
49
49
|
|
|
50
|
-
Composto
|
|
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
|
-
##
|
|
195
|
+
## Causal context
|
|
192
196
|
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.**
|
|
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: **
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
L0 compression: 97
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
2184
|
-
calibrationFactor(
|
|
2219
|
+
coverageFactor(averaged),
|
|
2220
|
+
calibrationFactor(averaged),
|
|
2185
2221
|
freshnessFactor(ctx),
|
|
2186
2222
|
historyFactor(ctx.totalCommits)
|
|
2187
2223
|
);
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
1988
|
-
calibrationFactor(
|
|
2023
|
+
coverageFactor(averaged),
|
|
2024
|
+
calibrationFactor(averaged),
|
|
1989
2025
|
freshnessFactor(ctx),
|
|
1990
2026
|
historyFactor(ctx.totalCommits)
|
|
1991
2027
|
);
|
package/dist/memory/api.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
423
|
-
calibrationFactor(
|
|
458
|
+
coverageFactor(averaged),
|
|
459
|
+
calibrationFactor(averaged),
|
|
424
460
|
freshnessFactor(ctx),
|
|
425
461
|
historyFactor(ctx.totalCommits)
|
|
426
462
|
);
|
package/dist/memory/worker.js
CHANGED
|
@@ -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);
|