composto-ai 0.7.1 → 0.8.2
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 +41 -37
- package/dist/index.js +71 -5
- package/dist/mcp/server.js +52 -5
- package/dist/memory/api.js +41 -5
- package/dist/memory/worker.js +21 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
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
|
-
|
|
5
|
+
> Send your agent the structure, not the noise.
|
|
6
|
+
|
|
7
|
+
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
8
|
|
|
7
9
|
```
|
|
8
|
-
$ composto
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
signals:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
#
|
|
10
|
+
$ composto ir src/memory/confidence.ts L1
|
|
11
|
+
|
|
12
|
+
USE:./types.js
|
|
13
|
+
OUT INTERFACE:ConfidenceContext
|
|
14
|
+
OUT INTERFACE:ScoreAndConfidence
|
|
15
|
+
FN:calibrationFactor(signals: Signal[])
|
|
16
|
+
GUARD:[firing.length === 0 → 1.0, avg < 20 → 0.3, avg < 100 → 0.6]
|
|
17
|
+
FN:historyFactor(totalCommits: number)
|
|
18
|
+
GUARD:[totalCommits < 50 → 0.2, totalCommits < 200 → 0.5, totalCommits < 1000 → 0.8]
|
|
19
|
+
OUT FN:computeScoreAndConfidence(signals: Signal[], ctx: ConfidenceContext)
|
|
20
|
+
|
|
21
|
+
# 541 tokens of raw code → 230 tokens of IR (57% fewer). Structure intact:
|
|
22
|
+
# every signature, dependency, and decision threshold survives.
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -45,9 +47,9 @@ composto impact src/auth/login.ts
|
|
|
45
47
|
composto index --status # diagnostics: schema, freshness, calibration
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
###
|
|
50
|
+
### The core: token-efficient structural context
|
|
49
51
|
|
|
50
|
-
Composto
|
|
52
|
+
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
53
|
|
|
52
54
|
```bash
|
|
53
55
|
composto ir src/app.ts # compress a file to IR (L0/L1/L2/L3)
|
|
@@ -57,6 +59,10 @@ composto benchmark . # see compression stats
|
|
|
57
59
|
|
|
58
60
|
See the [IR Layers](#ir-layers), [Health-Aware IR](#health-aware-ir), and [Context Budget](#context-budget) sections below for details.
|
|
59
61
|
|
|
62
|
+
### On top: causal history as advisory context
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
60
66
|
### MCP plugin (Claude Code, Cursor, Claude Desktop)
|
|
61
67
|
|
|
62
68
|
The MCP server is bundled inside `composto-ai`. Install the package globally first, then register the server with your client:
|
|
@@ -188,27 +194,25 @@ composto index --status # diagnostics: schema, freshness, calibration
|
|
|
188
194
|
|
|
189
195
|
---
|
|
190
196
|
|
|
191
|
-
##
|
|
197
|
+
## Causal context
|
|
192
198
|
|
|
193
|
-
|
|
199
|
+
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
200
|
|
|
195
|
-
|
|
201
|
+
Signals per query: `revert_match`, `hotspot`, `fix_ratio`, `author_churn`, `cochange`. The tool returns `unknown` when confidence is low rather than guessing.
|
|
196
202
|
|
|
197
203
|
```
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
hotspot · strength=0.00 precision=0.30
|
|
204
|
-
...
|
|
204
|
+
$ composto impact src/auth/login.ts
|
|
205
|
+
|
|
206
|
+
revert_match ■■■■■■■■■■ this file was touched by a Revert commit
|
|
207
|
+
cochange ■■■■■ historically co-changed with session.ts, token.ts in fixes
|
|
208
|
+
hotspot ■ 14 changes in the last 90 days
|
|
205
209
|
```
|
|
206
210
|
|
|
207
|
-
**Where we are, honestly.**
|
|
211
|
+
**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
212
|
|
|
209
|
-
The honest framing: **
|
|
213
|
+
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
214
|
|
|
211
|
-
|
|
215
|
+
Available as CLI (`composto impact`, `composto index`) and MCP tool (`composto_blastradius`, gated by `COMPOSTO_BLASTRADIUS=1` during beta).
|
|
212
216
|
|
|
213
217
|
---
|
|
214
218
|
|
|
@@ -307,14 +311,14 @@ Hotspot files get full detail. Everything else gets structure. Budget is never e
|
|
|
307
311
|
## Stats
|
|
308
312
|
|
|
309
313
|
```
|
|
310
|
-
|
|
311
|
-
L0 compression: 97
|
|
312
|
-
|
|
314
|
+
L1 compression: ~81% fewer tokens (full IR, structure preserved)
|
|
315
|
+
L0 compression: ~97% fewer tokens (structure map)
|
|
316
|
+
Token counts: verified against a real BPE tokenizer, not estimates
|
|
317
|
+
AST engine: AST-parsed, 0 regex fallback
|
|
313
318
|
Languages: TypeScript, JavaScript, Python, Go, Rust
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
in docs/blastradius-proof-v2.md.
|
|
319
|
+
Causal layer: high-recall advisory (0.67-0.80 recall on mature repos,
|
|
320
|
+
time-travel backtest across 4 public repos); precision
|
|
321
|
+
~0.55, surfaced as context not a gate.
|
|
318
322
|
```
|
|
319
323
|
|
|
320
324
|
---
|
package/dist/index.js
CHANGED
|
@@ -1010,6 +1010,17 @@ function emitTier3(node) {
|
|
|
1010
1010
|
if (expr.type === "await_expression") {
|
|
1011
1011
|
return null;
|
|
1012
1012
|
}
|
|
1013
|
+
if (expr.type === "assignment_expression") {
|
|
1014
|
+
if (node.parent?.type === "statement_block") return null;
|
|
1015
|
+
const left = expr.childForFieldName("left");
|
|
1016
|
+
const right = expr.childForFieldName("right");
|
|
1017
|
+
if (left?.type === "member_expression" && right && (right.type === "function_expression" || right.type === "arrow_function")) {
|
|
1018
|
+
const asyncPrefix = isAsync(right) ? "ASYNC " : "";
|
|
1019
|
+
const params = right.childForFieldName("parameters")?.text ?? "()";
|
|
1020
|
+
return `${asyncPrefix}FN:${left.text}${collapseText(params, 60)}`;
|
|
1021
|
+
}
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1013
1024
|
if (expr.type === "call_expression") {
|
|
1014
1025
|
const callee = expr.child(0)?.text ?? "";
|
|
1015
1026
|
if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
|
|
@@ -2125,13 +2136,38 @@ function computeAuthorChurn(db, filePath) {
|
|
|
2125
2136
|
return { ...base, strength };
|
|
2126
2137
|
}
|
|
2127
2138
|
|
|
2139
|
+
// src/memory/signals/cochange.ts
|
|
2140
|
+
var SATURATION_DEGREE = 10;
|
|
2141
|
+
var FALLBACK_PRECISION5 = 0.3;
|
|
2142
|
+
function computeCochange(db, filePath) {
|
|
2143
|
+
const row = db.prepare(`
|
|
2144
|
+
SELECT COUNT(DISTINCT ft2.file_path) AS degree
|
|
2145
|
+
FROM file_touches ft1
|
|
2146
|
+
JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
|
|
2147
|
+
JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
|
|
2148
|
+
WHERE ft1.file_path = ?
|
|
2149
|
+
`).get(filePath);
|
|
2150
|
+
const degree = row?.degree ?? 0;
|
|
2151
|
+
const strength = Math.min(1, degree / SATURATION_DEGREE);
|
|
2152
|
+
const cal = getCalibration(db, "cochange", FALLBACK_PRECISION5);
|
|
2153
|
+
return {
|
|
2154
|
+
type: "cochange",
|
|
2155
|
+
strength,
|
|
2156
|
+
precision: cal.precision,
|
|
2157
|
+
sample_size: cal.sampleSize,
|
|
2158
|
+
evidence: [],
|
|
2159
|
+
cochange_degree: degree
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2128
2163
|
// src/memory/signals/index.ts
|
|
2129
2164
|
function collectSignals(db, _repoPath, filePath) {
|
|
2130
2165
|
return [
|
|
2131
2166
|
computeRevertMatch(db, filePath),
|
|
2132
2167
|
computeHotspot(db, filePath),
|
|
2133
2168
|
computeFixRatio(db, filePath),
|
|
2134
|
-
computeAuthorChurn(db, filePath)
|
|
2169
|
+
computeAuthorChurn(db, filePath),
|
|
2170
|
+
computeCochange(db, filePath)
|
|
2135
2171
|
];
|
|
2136
2172
|
}
|
|
2137
2173
|
|
|
@@ -2170,18 +2206,29 @@ function historyFactor(totalCommits) {
|
|
|
2170
2206
|
if (totalCommits < 1e3) return 0.8;
|
|
2171
2207
|
return 1;
|
|
2172
2208
|
}
|
|
2209
|
+
var DEFAULT_COCHANGE_FLOOR = 1;
|
|
2210
|
+
function cochangeFloor() {
|
|
2211
|
+
const v = Number(process.env.COMPOSTO_COCHANGE_FLOOR);
|
|
2212
|
+
return Number.isFinite(v) && v >= 0 && v <= 1 ? v : DEFAULT_COCHANGE_FLOOR;
|
|
2213
|
+
}
|
|
2173
2214
|
function computeScoreAndConfidence(signals, ctx) {
|
|
2215
|
+
const cochange = signals.find((s) => s.type === "cochange");
|
|
2216
|
+
const averaged = signals.filter((s) => s.type !== "cochange");
|
|
2174
2217
|
let num = 0;
|
|
2175
2218
|
let den = 0;
|
|
2176
|
-
for (const s of
|
|
2219
|
+
for (const s of averaged) {
|
|
2177
2220
|
if (s.strength <= 0 || s.precision <= 0) continue;
|
|
2178
2221
|
num += s.strength * s.precision;
|
|
2179
2222
|
den += s.precision;
|
|
2180
2223
|
}
|
|
2181
|
-
|
|
2224
|
+
let score = den === 0 ? 0 : num / den;
|
|
2225
|
+
if (cochange) {
|
|
2226
|
+
const floor = cochangeFloor();
|
|
2227
|
+
score *= floor + (1 - floor) * cochange.strength;
|
|
2228
|
+
}
|
|
2182
2229
|
const confidence = Math.min(
|
|
2183
|
-
coverageFactor(
|
|
2184
|
-
calibrationFactor(
|
|
2230
|
+
coverageFactor(averaged),
|
|
2231
|
+
calibrationFactor(averaged),
|
|
2185
2232
|
freshnessFactor(ctx),
|
|
2186
2233
|
historyFactor(ctx.totalCommits)
|
|
2187
2234
|
);
|
|
@@ -2752,6 +2799,25 @@ async function runBenchmark(projectPath) {
|
|
|
2752
2799
|
console.log(` L1 (full IR): ${summary.totalRaw} \u2192 ${summary.totalIRL1} tokens (${summary.totalSavedPercent.toFixed(1)}% reduction)`);
|
|
2753
2800
|
console.log(` Files analyzed: ${summary.fileCount}`);
|
|
2754
2801
|
console.log(` Engine: ${summary.astCount} AST, ${summary.fpCount} FP`);
|
|
2802
|
+
printSavingsCard(summary);
|
|
2803
|
+
}
|
|
2804
|
+
function printSavingsCard(summary) {
|
|
2805
|
+
if (summary.totalRaw <= 0) return;
|
|
2806
|
+
const saved = summary.totalRaw - summary.totalIRL1;
|
|
2807
|
+
const pct2 = summary.totalSavedPercent.toFixed(1);
|
|
2808
|
+
const SONNET_PER_MTOK = 3;
|
|
2809
|
+
const LOADS_PER_DAY = 50;
|
|
2810
|
+
const DAYS = 30;
|
|
2811
|
+
const rawCost = summary.totalRaw / 1e6 * SONNET_PER_MTOK;
|
|
2812
|
+
const irCost = summary.totalIRL1 / 1e6 * SONNET_PER_MTOK;
|
|
2813
|
+
const monthly = (rawCost - irCost) * LOADS_PER_DAY * DAYS;
|
|
2814
|
+
console.log("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2815
|
+
console.log(` \u{1F4B8} Every full-context load of this project: $${rawCost.toFixed(2)} raw \u2192 $${irCost.toFixed(2)} with Composto`);
|
|
2816
|
+
console.log(` (Claude Sonnet input, $3/Mtok). At ${LOADS_PER_DAY} loads/day that is ~$${monthly.toFixed(0)}/month saved.`);
|
|
2817
|
+
console.log("\n \u{1F4CB} Share your result:");
|
|
2818
|
+
console.log(` Composto compressed my ${summary.fileCount}-file project ${pct2}% (${summary.totalRaw.toLocaleString()} \u2192 ${summary.totalIRL1.toLocaleString()} tokens).`);
|
|
2819
|
+
console.log(" Try yours: npm i -g composto-ai && composto benchmark .");
|
|
2820
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2755
2821
|
}
|
|
2756
2822
|
async function runBenchmarkQuality(projectPath, filePath) {
|
|
2757
2823
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
package/dist/mcp/server.js
CHANGED
|
@@ -818,6 +818,17 @@ function emitTier3(node) {
|
|
|
818
818
|
if (expr.type === "await_expression") {
|
|
819
819
|
return null;
|
|
820
820
|
}
|
|
821
|
+
if (expr.type === "assignment_expression") {
|
|
822
|
+
if (node.parent?.type === "statement_block") return null;
|
|
823
|
+
const left = expr.childForFieldName("left");
|
|
824
|
+
const right = expr.childForFieldName("right");
|
|
825
|
+
if (left?.type === "member_expression" && right && (right.type === "function_expression" || right.type === "arrow_function")) {
|
|
826
|
+
const asyncPrefix = isAsync(right) ? "ASYNC " : "";
|
|
827
|
+
const params = right.childForFieldName("parameters")?.text ?? "()";
|
|
828
|
+
return `${asyncPrefix}FN:${left.text}${collapseText(params, 60)}`;
|
|
829
|
+
}
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
821
832
|
if (expr.type === "call_expression") {
|
|
822
833
|
const callee = expr.child(0)?.text ?? "";
|
|
823
834
|
if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
|
|
@@ -1929,13 +1940,38 @@ function computeAuthorChurn(db, filePath) {
|
|
|
1929
1940
|
return { ...base, strength };
|
|
1930
1941
|
}
|
|
1931
1942
|
|
|
1943
|
+
// src/memory/signals/cochange.ts
|
|
1944
|
+
var SATURATION_DEGREE = 10;
|
|
1945
|
+
var FALLBACK_PRECISION5 = 0.3;
|
|
1946
|
+
function computeCochange(db, filePath) {
|
|
1947
|
+
const row = db.prepare(`
|
|
1948
|
+
SELECT COUNT(DISTINCT ft2.file_path) AS degree
|
|
1949
|
+
FROM file_touches ft1
|
|
1950
|
+
JOIN commits c ON c.sha = ft1.commit_sha AND c.is_fix = 1
|
|
1951
|
+
JOIN file_touches ft2 ON ft2.commit_sha = ft1.commit_sha AND ft2.file_path != ft1.file_path
|
|
1952
|
+
WHERE ft1.file_path = ?
|
|
1953
|
+
`).get(filePath);
|
|
1954
|
+
const degree = row?.degree ?? 0;
|
|
1955
|
+
const strength = Math.min(1, degree / SATURATION_DEGREE);
|
|
1956
|
+
const cal = getCalibration(db, "cochange", FALLBACK_PRECISION5);
|
|
1957
|
+
return {
|
|
1958
|
+
type: "cochange",
|
|
1959
|
+
strength,
|
|
1960
|
+
precision: cal.precision,
|
|
1961
|
+
sample_size: cal.sampleSize,
|
|
1962
|
+
evidence: [],
|
|
1963
|
+
cochange_degree: degree
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1932
1967
|
// src/memory/signals/index.ts
|
|
1933
1968
|
function collectSignals(db, _repoPath, filePath) {
|
|
1934
1969
|
return [
|
|
1935
1970
|
computeRevertMatch(db, filePath),
|
|
1936
1971
|
computeHotspot(db, filePath),
|
|
1937
1972
|
computeFixRatio(db, filePath),
|
|
1938
|
-
computeAuthorChurn(db, filePath)
|
|
1973
|
+
computeAuthorChurn(db, filePath),
|
|
1974
|
+
computeCochange(db, filePath)
|
|
1939
1975
|
];
|
|
1940
1976
|
}
|
|
1941
1977
|
|
|
@@ -1974,18 +2010,29 @@ function historyFactor(totalCommits) {
|
|
|
1974
2010
|
if (totalCommits < 1e3) return 0.8;
|
|
1975
2011
|
return 1;
|
|
1976
2012
|
}
|
|
2013
|
+
var DEFAULT_COCHANGE_FLOOR = 1;
|
|
2014
|
+
function cochangeFloor() {
|
|
2015
|
+
const v = Number(process.env.COMPOSTO_COCHANGE_FLOOR);
|
|
2016
|
+
return Number.isFinite(v) && v >= 0 && v <= 1 ? v : DEFAULT_COCHANGE_FLOOR;
|
|
2017
|
+
}
|
|
1977
2018
|
function computeScoreAndConfidence(signals, ctx) {
|
|
2019
|
+
const cochange = signals.find((s) => s.type === "cochange");
|
|
2020
|
+
const averaged = signals.filter((s) => s.type !== "cochange");
|
|
1978
2021
|
let num = 0;
|
|
1979
2022
|
let den = 0;
|
|
1980
|
-
for (const s of
|
|
2023
|
+
for (const s of averaged) {
|
|
1981
2024
|
if (s.strength <= 0 || s.precision <= 0) continue;
|
|
1982
2025
|
num += s.strength * s.precision;
|
|
1983
2026
|
den += s.precision;
|
|
1984
2027
|
}
|
|
1985
|
-
|
|
2028
|
+
let score = den === 0 ? 0 : num / den;
|
|
2029
|
+
if (cochange) {
|
|
2030
|
+
const floor = cochangeFloor();
|
|
2031
|
+
score *= floor + (1 - floor) * cochange.strength;
|
|
2032
|
+
}
|
|
1986
2033
|
const confidence = Math.min(
|
|
1987
|
-
coverageFactor(
|
|
1988
|
-
calibrationFactor(
|
|
2034
|
+
coverageFactor(averaged),
|
|
2035
|
+
calibrationFactor(averaged),
|
|
1989
2036
|
freshnessFactor(ctx),
|
|
1990
2037
|
historyFactor(ctx.totalCommits)
|
|
1991
2038
|
);
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "composto-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Proactive AI team companion — less tokens, more insight",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
21
22
|
"test": "vitest run",
|
|
22
23
|
"test:watch": "vitest",
|
|
23
24
|
"dev": "tsx src/index.ts"
|