composto-ai 0.7.0 → 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 +95 -29
- package/dist/mcp/server.js +99 -29
- 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
|
@@ -843,8 +843,9 @@ function emitTier1(node) {
|
|
|
843
843
|
const outPrefix = exported ? "OUT " : "";
|
|
844
844
|
switch (node.type) {
|
|
845
845
|
case "import_statement": {
|
|
846
|
-
const
|
|
847
|
-
return `USE:${
|
|
846
|
+
const source = node.childForFieldName("source")?.text;
|
|
847
|
+
if (source) return `USE:${source.slice(1, -1)}`;
|
|
848
|
+
return `USE:${collapseText(node.text, 80)}`;
|
|
848
849
|
}
|
|
849
850
|
case "function_declaration": {
|
|
850
851
|
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
@@ -974,9 +975,30 @@ function emitTier3(node) {
|
|
|
974
975
|
}
|
|
975
976
|
if (node.parent?.type === "statement_block") return null;
|
|
976
977
|
const vt = value.type;
|
|
977
|
-
if (vt === "number" || vt === "true" || vt === "false")
|
|
978
|
-
|
|
979
|
-
|
|
978
|
+
if (vt === "number" || vt === "true" || vt === "false") {
|
|
979
|
+
return `VAR:${name} = ${value.text}`;
|
|
980
|
+
}
|
|
981
|
+
if (vt === "array") {
|
|
982
|
+
return `VAR:${name}[${value.namedChildCount}]`;
|
|
983
|
+
}
|
|
984
|
+
if (vt === "object") {
|
|
985
|
+
const keys = [];
|
|
986
|
+
for (let i = 0; i < value.namedChildCount; i++) {
|
|
987
|
+
const member = value.namedChild(i);
|
|
988
|
+
keys.push(member.childForFieldName("key")?.text ?? member.text);
|
|
989
|
+
if (keys.length >= 6) break;
|
|
990
|
+
}
|
|
991
|
+
const more = value.namedChildCount > keys.length ? ", ..." : "";
|
|
992
|
+
return `VAR:${name}{${collapseText(keys.join(", "), 50)}${more}}`;
|
|
993
|
+
}
|
|
994
|
+
if (vt === "new_expression") {
|
|
995
|
+
const ctor = value.childForFieldName("constructor")?.text ?? "?";
|
|
996
|
+
return `VAR:${name} = new ${ctor}(...)`;
|
|
997
|
+
}
|
|
998
|
+
if (vt === "call_expression") {
|
|
999
|
+
const callee = value.childForFieldName("function")?.text ?? "?";
|
|
1000
|
+
return `VAR:${name} = ${collapseText(callee, 40)}(...)`;
|
|
1001
|
+
}
|
|
980
1002
|
const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
|
|
981
1003
|
return `VAR:${name} = ${collapseText(valText, 50)}`;
|
|
982
1004
|
}
|
|
@@ -1137,29 +1159,27 @@ async function astWalkIR(code, filePath) {
|
|
|
1137
1159
|
}
|
|
1138
1160
|
const merged = [];
|
|
1139
1161
|
let guardBlock = [];
|
|
1162
|
+
const guardValue = (ret) => ret.replace(/^RET\s+/, "");
|
|
1163
|
+
const flushGuards = () => {
|
|
1164
|
+
if (guardBlock.length === 0) return;
|
|
1165
|
+
if (guardBlock.length < 3) {
|
|
1166
|
+
for (const g of guardBlock) merged.push(`${g.indent}IF:${g.cond} \u2192 ${g.ret}`);
|
|
1167
|
+
} else {
|
|
1168
|
+
const entries = guardBlock.map((g) => `${g.cond} \u2192 ${guardValue(g.ret)}`);
|
|
1169
|
+
merged.push(`${guardBlock[0].indent}GUARD:[${entries.join(", ")}]`);
|
|
1170
|
+
}
|
|
1171
|
+
guardBlock = [];
|
|
1172
|
+
};
|
|
1140
1173
|
for (const line of pass1) {
|
|
1141
|
-
const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192
|
|
1174
|
+
const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 (.+)$/);
|
|
1142
1175
|
if (guardMatch) {
|
|
1143
|
-
guardBlock.push(guardMatch[2].trim());
|
|
1176
|
+
guardBlock.push({ indent: guardMatch[1], cond: guardMatch[2].trim(), ret: guardMatch[3].trim() });
|
|
1144
1177
|
continue;
|
|
1145
1178
|
}
|
|
1146
|
-
|
|
1147
|
-
if (guardBlock.length < 3) {
|
|
1148
|
-
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
1149
|
-
} else {
|
|
1150
|
-
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
1151
|
-
}
|
|
1152
|
-
guardBlock = [];
|
|
1153
|
-
}
|
|
1179
|
+
flushGuards();
|
|
1154
1180
|
merged.push(line);
|
|
1155
1181
|
}
|
|
1156
|
-
|
|
1157
|
-
if (guardBlock.length < 3) {
|
|
1158
|
-
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
1159
|
-
} else {
|
|
1160
|
-
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1182
|
+
flushGuards();
|
|
1163
1183
|
return merged.join("\n");
|
|
1164
1184
|
}
|
|
1165
1185
|
|
|
@@ -2105,13 +2125,38 @@ function computeAuthorChurn(db, filePath) {
|
|
|
2105
2125
|
return { ...base, strength };
|
|
2106
2126
|
}
|
|
2107
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
|
+
|
|
2108
2152
|
// src/memory/signals/index.ts
|
|
2109
2153
|
function collectSignals(db, _repoPath, filePath) {
|
|
2110
2154
|
return [
|
|
2111
2155
|
computeRevertMatch(db, filePath),
|
|
2112
2156
|
computeHotspot(db, filePath),
|
|
2113
2157
|
computeFixRatio(db, filePath),
|
|
2114
|
-
computeAuthorChurn(db, filePath)
|
|
2158
|
+
computeAuthorChurn(db, filePath),
|
|
2159
|
+
computeCochange(db, filePath)
|
|
2115
2160
|
];
|
|
2116
2161
|
}
|
|
2117
2162
|
|
|
@@ -2150,18 +2195,29 @@ function historyFactor(totalCommits) {
|
|
|
2150
2195
|
if (totalCommits < 1e3) return 0.8;
|
|
2151
2196
|
return 1;
|
|
2152
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
|
+
}
|
|
2153
2203
|
function computeScoreAndConfidence(signals, ctx) {
|
|
2204
|
+
const cochange = signals.find((s) => s.type === "cochange");
|
|
2205
|
+
const averaged = signals.filter((s) => s.type !== "cochange");
|
|
2154
2206
|
let num = 0;
|
|
2155
2207
|
let den = 0;
|
|
2156
|
-
for (const s of
|
|
2208
|
+
for (const s of averaged) {
|
|
2157
2209
|
if (s.strength <= 0 || s.precision <= 0) continue;
|
|
2158
2210
|
num += s.strength * s.precision;
|
|
2159
2211
|
den += s.precision;
|
|
2160
2212
|
}
|
|
2161
|
-
|
|
2213
|
+
let score = den === 0 ? 0 : num / den;
|
|
2214
|
+
if (cochange) {
|
|
2215
|
+
const floor = cochangeFloor();
|
|
2216
|
+
score *= floor + (1 - floor) * cochange.strength;
|
|
2217
|
+
}
|
|
2162
2218
|
const confidence = Math.min(
|
|
2163
|
-
coverageFactor(
|
|
2164
|
-
calibrationFactor(
|
|
2219
|
+
coverageFactor(averaged),
|
|
2220
|
+
calibrationFactor(averaged),
|
|
2165
2221
|
freshnessFactor(ctx),
|
|
2166
2222
|
historyFactor(ctx.totalCommits)
|
|
2167
2223
|
);
|
|
@@ -3593,7 +3649,16 @@ function renderSummary(s) {
|
|
|
3593
3649
|
}
|
|
3594
3650
|
|
|
3595
3651
|
// src/index.ts
|
|
3652
|
+
import { createRequire } from "module";
|
|
3596
3653
|
import { join as join13, resolve as resolve2 } from "path";
|
|
3654
|
+
var PKG_VERSION = (() => {
|
|
3655
|
+
try {
|
|
3656
|
+
const req = createRequire(import.meta.url);
|
|
3657
|
+
return req("../package.json").version;
|
|
3658
|
+
} catch {
|
|
3659
|
+
return "0.0.0";
|
|
3660
|
+
}
|
|
3661
|
+
})();
|
|
3597
3662
|
async function readStdin() {
|
|
3598
3663
|
if (process.stdin.isTTY) return "";
|
|
3599
3664
|
const chunks = [];
|
|
@@ -3754,10 +3819,11 @@ switch (command) {
|
|
|
3754
3819
|
break;
|
|
3755
3820
|
}
|
|
3756
3821
|
case "version":
|
|
3757
|
-
console.log(
|
|
3822
|
+
console.log(`composto v${PKG_VERSION}`);
|
|
3758
3823
|
break;
|
|
3759
3824
|
default:
|
|
3760
|
-
console.log(
|
|
3825
|
+
console.log(`composto v${PKG_VERSION} \u2014 less tokens, more insight
|
|
3826
|
+
`);
|
|
3761
3827
|
console.log("Commands:");
|
|
3762
3828
|
console.log(" scan [path] Scan codebase for issues");
|
|
3763
3829
|
console.log(" trends [path] Analyze codebase health trends");
|
package/dist/mcp/server.js
CHANGED
|
@@ -5,7 +5,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { readFileSync as readFileSync4 } from "fs";
|
|
8
|
-
import {
|
|
8
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9
|
+
import { resolve as resolve2, relative as relative2, join as join6, dirname as dirname5 } from "path";
|
|
9
10
|
|
|
10
11
|
// src/ir/structure.ts
|
|
11
12
|
var CLASSIFIERS = [
|
|
@@ -650,8 +651,9 @@ function emitTier1(node) {
|
|
|
650
651
|
const outPrefix = exported ? "OUT " : "";
|
|
651
652
|
switch (node.type) {
|
|
652
653
|
case "import_statement": {
|
|
653
|
-
const
|
|
654
|
-
return `USE:${
|
|
654
|
+
const source = node.childForFieldName("source")?.text;
|
|
655
|
+
if (source) return `USE:${source.slice(1, -1)}`;
|
|
656
|
+
return `USE:${collapseText(node.text, 80)}`;
|
|
655
657
|
}
|
|
656
658
|
case "function_declaration": {
|
|
657
659
|
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
@@ -781,9 +783,30 @@ function emitTier3(node) {
|
|
|
781
783
|
}
|
|
782
784
|
if (node.parent?.type === "statement_block") return null;
|
|
783
785
|
const vt = value.type;
|
|
784
|
-
if (vt === "number" || vt === "true" || vt === "false")
|
|
785
|
-
|
|
786
|
-
|
|
786
|
+
if (vt === "number" || vt === "true" || vt === "false") {
|
|
787
|
+
return `VAR:${name} = ${value.text}`;
|
|
788
|
+
}
|
|
789
|
+
if (vt === "array") {
|
|
790
|
+
return `VAR:${name}[${value.namedChildCount}]`;
|
|
791
|
+
}
|
|
792
|
+
if (vt === "object") {
|
|
793
|
+
const keys = [];
|
|
794
|
+
for (let i = 0; i < value.namedChildCount; i++) {
|
|
795
|
+
const member = value.namedChild(i);
|
|
796
|
+
keys.push(member.childForFieldName("key")?.text ?? member.text);
|
|
797
|
+
if (keys.length >= 6) break;
|
|
798
|
+
}
|
|
799
|
+
const more = value.namedChildCount > keys.length ? ", ..." : "";
|
|
800
|
+
return `VAR:${name}{${collapseText(keys.join(", "), 50)}${more}}`;
|
|
801
|
+
}
|
|
802
|
+
if (vt === "new_expression") {
|
|
803
|
+
const ctor = value.childForFieldName("constructor")?.text ?? "?";
|
|
804
|
+
return `VAR:${name} = new ${ctor}(...)`;
|
|
805
|
+
}
|
|
806
|
+
if (vt === "call_expression") {
|
|
807
|
+
const callee = value.childForFieldName("function")?.text ?? "?";
|
|
808
|
+
return `VAR:${name} = ${collapseText(callee, 40)}(...)`;
|
|
809
|
+
}
|
|
787
810
|
const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
|
|
788
811
|
return `VAR:${name} = ${collapseText(valText, 50)}`;
|
|
789
812
|
}
|
|
@@ -944,29 +967,27 @@ async function astWalkIR(code, filePath) {
|
|
|
944
967
|
}
|
|
945
968
|
const merged = [];
|
|
946
969
|
let guardBlock = [];
|
|
970
|
+
const guardValue = (ret) => ret.replace(/^RET\s+/, "");
|
|
971
|
+
const flushGuards = () => {
|
|
972
|
+
if (guardBlock.length === 0) return;
|
|
973
|
+
if (guardBlock.length < 3) {
|
|
974
|
+
for (const g of guardBlock) merged.push(`${g.indent}IF:${g.cond} \u2192 ${g.ret}`);
|
|
975
|
+
} else {
|
|
976
|
+
const entries = guardBlock.map((g) => `${g.cond} \u2192 ${guardValue(g.ret)}`);
|
|
977
|
+
merged.push(`${guardBlock[0].indent}GUARD:[${entries.join(", ")}]`);
|
|
978
|
+
}
|
|
979
|
+
guardBlock = [];
|
|
980
|
+
};
|
|
947
981
|
for (const line of pass1) {
|
|
948
|
-
const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192
|
|
982
|
+
const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 (.+)$/);
|
|
949
983
|
if (guardMatch) {
|
|
950
|
-
guardBlock.push(guardMatch[2].trim());
|
|
984
|
+
guardBlock.push({ indent: guardMatch[1], cond: guardMatch[2].trim(), ret: guardMatch[3].trim() });
|
|
951
985
|
continue;
|
|
952
986
|
}
|
|
953
|
-
|
|
954
|
-
if (guardBlock.length < 3) {
|
|
955
|
-
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
956
|
-
} else {
|
|
957
|
-
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
958
|
-
}
|
|
959
|
-
guardBlock = [];
|
|
960
|
-
}
|
|
987
|
+
flushGuards();
|
|
961
988
|
merged.push(line);
|
|
962
989
|
}
|
|
963
|
-
|
|
964
|
-
if (guardBlock.length < 3) {
|
|
965
|
-
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
966
|
-
} else {
|
|
967
|
-
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
990
|
+
flushGuards();
|
|
970
991
|
return merged.join("\n");
|
|
971
992
|
}
|
|
972
993
|
|
|
@@ -1908,13 +1929,38 @@ function computeAuthorChurn(db, filePath) {
|
|
|
1908
1929
|
return { ...base, strength };
|
|
1909
1930
|
}
|
|
1910
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
|
+
|
|
1911
1956
|
// src/memory/signals/index.ts
|
|
1912
1957
|
function collectSignals(db, _repoPath, filePath) {
|
|
1913
1958
|
return [
|
|
1914
1959
|
computeRevertMatch(db, filePath),
|
|
1915
1960
|
computeHotspot(db, filePath),
|
|
1916
1961
|
computeFixRatio(db, filePath),
|
|
1917
|
-
computeAuthorChurn(db, filePath)
|
|
1962
|
+
computeAuthorChurn(db, filePath),
|
|
1963
|
+
computeCochange(db, filePath)
|
|
1918
1964
|
];
|
|
1919
1965
|
}
|
|
1920
1966
|
|
|
@@ -1953,18 +1999,29 @@ function historyFactor(totalCommits) {
|
|
|
1953
1999
|
if (totalCommits < 1e3) return 0.8;
|
|
1954
2000
|
return 1;
|
|
1955
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
|
+
}
|
|
1956
2007
|
function computeScoreAndConfidence(signals, ctx) {
|
|
2008
|
+
const cochange = signals.find((s) => s.type === "cochange");
|
|
2009
|
+
const averaged = signals.filter((s) => s.type !== "cochange");
|
|
1957
2010
|
let num = 0;
|
|
1958
2011
|
let den = 0;
|
|
1959
|
-
for (const s of
|
|
2012
|
+
for (const s of averaged) {
|
|
1960
2013
|
if (s.strength <= 0 || s.precision <= 0) continue;
|
|
1961
2014
|
num += s.strength * s.precision;
|
|
1962
2015
|
den += s.precision;
|
|
1963
2016
|
}
|
|
1964
|
-
|
|
2017
|
+
let score = den === 0 ? 0 : num / den;
|
|
2018
|
+
if (cochange) {
|
|
2019
|
+
const floor = cochangeFloor();
|
|
2020
|
+
score *= floor + (1 - floor) * cochange.strength;
|
|
2021
|
+
}
|
|
1965
2022
|
const confidence = Math.min(
|
|
1966
|
-
coverageFactor(
|
|
1967
|
-
calibrationFactor(
|
|
2023
|
+
coverageFactor(averaged),
|
|
2024
|
+
calibrationFactor(averaged),
|
|
1968
2025
|
freshnessFactor(ctx),
|
|
1969
2026
|
historyFactor(ctx.totalCommits)
|
|
1970
2027
|
);
|
|
@@ -2381,9 +2438,22 @@ var MemoryAPI = class {
|
|
|
2381
2438
|
|
|
2382
2439
|
// src/mcp/server.ts
|
|
2383
2440
|
var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
|
|
2441
|
+
var PKG_VERSION = (() => {
|
|
2442
|
+
try {
|
|
2443
|
+
const pkgPath = join6(
|
|
2444
|
+
dirname5(fileURLToPath3(import.meta.url)),
|
|
2445
|
+
"..",
|
|
2446
|
+
"..",
|
|
2447
|
+
"package.json"
|
|
2448
|
+
);
|
|
2449
|
+
return JSON.parse(readFileSync4(pkgPath, "utf-8")).version;
|
|
2450
|
+
} catch {
|
|
2451
|
+
return "0.0.0";
|
|
2452
|
+
}
|
|
2453
|
+
})();
|
|
2384
2454
|
var server = new McpServer({
|
|
2385
2455
|
name: "composto",
|
|
2386
|
-
version:
|
|
2456
|
+
version: PKG_VERSION
|
|
2387
2457
|
});
|
|
2388
2458
|
server.tool(
|
|
2389
2459
|
"composto_ir",
|
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);
|