@reposkein/mcp 0.7.0 → 0.9.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 +1 -0
- package/binary-digests.json +4 -4
- package/dist/SKILL.md +22 -0
- package/dist/cli/doctor.d.ts +2 -2
- package/dist/cli/doctor.js +8 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/doctorFreshness.d.ts +6 -0
- package/dist/cli/doctorFreshness.js +23 -0
- package/dist/cli/doctorFreshness.js.map +1 -1
- package/dist/cli/migrate.d.ts +26 -0
- package/dist/cli/migrate.js +208 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/embed/batch.d.ts +47 -0
- package/dist/embed/batch.js +92 -0
- package/dist/embed/batch.js.map +1 -0
- package/dist/embed/cache.d.ts +22 -48
- package/dist/embed/cache.js +44 -164
- package/dist/embed/cache.js.map +1 -1
- package/dist/embed/corpusVectors.d.ts +20 -0
- package/dist/embed/corpusVectors.js +60 -0
- package/dist/embed/corpusVectors.js.map +1 -0
- package/dist/embed/provider.d.ts +21 -2
- package/dist/embed/provider.js.map +1 -1
- package/dist/embed/providers/http.d.ts +3 -2
- package/dist/embed/providers/http.js +14 -1
- package/dist/embed/providers/http.js.map +1 -1
- package/dist/embed/providers/voyage.d.ts +3 -2
- package/dist/embed/providers/voyage.js +50 -54
- package/dist/embed/providers/voyage.js.map +1 -1
- package/dist/embed/vectorStore.d.ts +89 -0
- package/dist/embed/vectorStore.js +267 -0
- package/dist/embed/vectorStore.js.map +1 -0
- package/dist/guard/caps.d.ts +3 -0
- package/dist/guard/caps.js +4 -1
- package/dist/guard/caps.js.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/search/bm25f.d.ts +17 -1
- package/dist/search/bm25f.js +89 -53
- package/dist/search/bm25f.js.map +1 -1
- package/dist/store/GraphStore.d.ts +6 -1
- package/dist/store/JsonlGraphStore.d.ts +47 -10
- package/dist/store/JsonlGraphStore.js +129 -50
- package/dist/store/JsonlGraphStore.js.map +1 -1
- package/dist/store/Neo4jGraphStore.d.ts +11 -0
- package/dist/store/Neo4jGraphStore.js +29 -2
- package/dist/store/Neo4jGraphStore.js.map +1 -1
- package/dist/store/instrumentTool.d.ts +9 -1
- package/dist/store/instrumentTool.js +33 -2
- package/dist/store/instrumentTool.js.map +1 -1
- package/dist/store/jsonlGraph.d.ts +22 -0
- package/dist/store/jsonlGraph.js +75 -18
- package/dist/store/jsonlGraph.js.map +1 -1
- package/dist/store/jsonlLines.d.ts +21 -0
- package/dist/store/jsonlLines.js +64 -0
- package/dist/store/jsonlLines.js.map +1 -0
- package/dist/store/repoSession.d.ts +19 -0
- package/dist/store/repoSession.js +25 -0
- package/dist/store/repoSession.js.map +1 -1
- package/dist/store/trackedGraph.d.ts +14 -0
- package/dist/store/trackedGraph.js +39 -0
- package/dist/store/trackedGraph.js.map +1 -0
- package/dist/tools/readCypher.js +9 -2
- package/dist/tools/readCypher.js.map +1 -1
- package/dist/tools/semanticFind.js +9 -6
- package/dist/tools/semanticFind.js.map +1 -1
- package/package.json +1 -1
package/dist/search/bm25f.d.ts
CHANGED
|
@@ -27,5 +27,21 @@ export declare function tokenize(s: string): string[];
|
|
|
27
27
|
/**
|
|
28
28
|
* Rank corpus nodes by BM25F for the given query, returning up to limit results.
|
|
29
29
|
* Deterministic: identical corpus + query → identical ranking.
|
|
30
|
+
*
|
|
31
|
+
* Takes an ITERABLE, and iterates it twice — BM25F needs corpus-wide statistics
|
|
32
|
+
* (average field lengths, document frequencies) before any document can be
|
|
33
|
+
* scored, so the corpus cannot be consumed in a single pass. What it does NOT
|
|
34
|
+
* do any more is RETAIN the corpus: the old form built a tokenized copy of all
|
|
35
|
+
* five fields of every node and held it through scoring, then collected every
|
|
36
|
+
* positive-scoring document before sorting. Retention is now bounded by
|
|
37
|
+
* `limit`; tokens are discarded as soon as a document has been scored.
|
|
38
|
+
*
|
|
39
|
+
* The two passes cost tokenization twice. That is the trade: CPU proportional
|
|
40
|
+
* to the corpus, memory proportional to the limit — which is the right way
|
|
41
|
+
* round for a query path that runs inside an agent's tool call.
|
|
42
|
+
*
|
|
43
|
+
* The iterable must be re-iterable (an array, or a generator function's result
|
|
44
|
+
* re-invoked). Results are identical to the single-pass form: same maths, and
|
|
45
|
+
* the same total ordering (score desc, id asc) applied to a bounded heap.
|
|
30
46
|
*/
|
|
31
|
-
export declare function rankCorpus(corpus: CorpusNode
|
|
47
|
+
export declare function rankCorpus(corpus: Iterable<CorpusNode>, query: string, limit: number): Scored[];
|
package/dist/search/bm25f.js
CHANGED
|
@@ -57,58 +57,93 @@ export function tokenize(s) {
|
|
|
57
57
|
function fieldTokens(value) {
|
|
58
58
|
return tokenize(value);
|
|
59
59
|
}
|
|
60
|
+
/** Count occurrences without allocating. The old form was
|
|
61
|
+
* `toks.filter((t) => t === qt).length`, which built a throwaway array for
|
|
62
|
+
* every (document x field x query token) — millions of them on a large
|
|
63
|
+
* corpus, for a number. */
|
|
64
|
+
function countOccurrences(toks, needle) {
|
|
65
|
+
let n = 0;
|
|
66
|
+
for (const t of toks)
|
|
67
|
+
if (t === needle)
|
|
68
|
+
n++;
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
/** Tokenize one node's fields. Callers must NOT retain the result. */
|
|
72
|
+
function tokenizeFields(node) {
|
|
73
|
+
const fields = {};
|
|
74
|
+
const lengths = {};
|
|
75
|
+
for (const f of FIELDS) {
|
|
76
|
+
const toks = fieldTokens(node[f] ?? "");
|
|
77
|
+
fields[f] = toks;
|
|
78
|
+
lengths[f] = toks.length;
|
|
79
|
+
}
|
|
80
|
+
return { fields, lengths };
|
|
81
|
+
}
|
|
60
82
|
/**
|
|
61
83
|
* Rank corpus nodes by BM25F for the given query, returning up to limit results.
|
|
62
84
|
* Deterministic: identical corpus + query → identical ranking.
|
|
85
|
+
*
|
|
86
|
+
* Takes an ITERABLE, and iterates it twice — BM25F needs corpus-wide statistics
|
|
87
|
+
* (average field lengths, document frequencies) before any document can be
|
|
88
|
+
* scored, so the corpus cannot be consumed in a single pass. What it does NOT
|
|
89
|
+
* do any more is RETAIN the corpus: the old form built a tokenized copy of all
|
|
90
|
+
* five fields of every node and held it through scoring, then collected every
|
|
91
|
+
* positive-scoring document before sorting. Retention is now bounded by
|
|
92
|
+
* `limit`; tokens are discarded as soon as a document has been scored.
|
|
93
|
+
*
|
|
94
|
+
* The two passes cost tokenization twice. That is the trade: CPU proportional
|
|
95
|
+
* to the corpus, memory proportional to the limit — which is the right way
|
|
96
|
+
* round for a query path that runs inside an agent's tool call.
|
|
97
|
+
*
|
|
98
|
+
* The iterable must be re-iterable (an array, or a generator function's result
|
|
99
|
+
* re-invoked). Results are identical to the single-pass form: same maths, and
|
|
100
|
+
* the same total ordering (score desc, id asc) applied to a bounded heap.
|
|
63
101
|
*/
|
|
64
102
|
export function rankCorpus(corpus, query, limit) {
|
|
65
|
-
if (
|
|
103
|
+
if (limit <= 0 || !query.trim())
|
|
66
104
|
return [];
|
|
67
|
-
// 1. Tokenize query
|
|
68
105
|
const queryTokens = tokenize(query);
|
|
69
106
|
if (queryTokens.length === 0)
|
|
70
107
|
return [];
|
|
71
108
|
const uniqueQueryTokens = [...new Set(queryTokens)];
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const value = node[f] ?? "";
|
|
78
|
-
const toks = fieldTokens(value);
|
|
79
|
-
fields[f] = toks;
|
|
80
|
-
fieldLengths[f] = toks.length;
|
|
81
|
-
}
|
|
82
|
-
return { node, fields, fieldLengths };
|
|
83
|
-
});
|
|
84
|
-
// 3. Compute average field lengths across corpus
|
|
85
|
-
const avgFieldLen = {};
|
|
86
|
-
for (const f of FIELDS) {
|
|
87
|
-
const total = docs.reduce((sum, d) => sum + d.fieldLengths[f], 0);
|
|
88
|
-
avgFieldLen[f] = docs.length > 0 ? total / docs.length : 1;
|
|
89
|
-
}
|
|
90
|
-
// 4. Build inverted index: token → set of doc indices (for IDF)
|
|
109
|
+
// Pass 1 — corpus statistics only. Field-length totals and document
|
|
110
|
+
// frequencies; every token is dropped as soon as it has been counted.
|
|
111
|
+
const totalFieldLen = {};
|
|
112
|
+
for (const f of FIELDS)
|
|
113
|
+
totalFieldLen[f] = 0;
|
|
91
114
|
const dfMap = new Map();
|
|
92
|
-
for (const qt of uniqueQueryTokens)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
for (const qt of uniqueQueryTokens)
|
|
116
|
+
dfMap.set(qt, 0);
|
|
117
|
+
let N = 0;
|
|
118
|
+
for (const node of corpus) {
|
|
119
|
+
N++;
|
|
120
|
+
const { fields, lengths } = tokenizeFields(node);
|
|
121
|
+
for (const f of FIELDS)
|
|
122
|
+
totalFieldLen[f] += lengths[f];
|
|
123
|
+
for (const qt of uniqueQueryTokens) {
|
|
97
124
|
for (const f of FIELDS) {
|
|
98
|
-
if (
|
|
99
|
-
|
|
125
|
+
if (fields[f].includes(qt)) {
|
|
126
|
+
dfMap.set(qt, dfMap.get(qt) + 1);
|
|
100
127
|
break;
|
|
101
128
|
}
|
|
102
129
|
}
|
|
103
|
-
if (found)
|
|
104
|
-
df++;
|
|
105
130
|
}
|
|
106
|
-
dfMap.set(qt, df);
|
|
107
131
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
for (const
|
|
132
|
+
if (N === 0)
|
|
133
|
+
return [];
|
|
134
|
+
const avgFieldLen = {};
|
|
135
|
+
for (const f of FIELDS)
|
|
136
|
+
avgFieldLen[f] = totalFieldLen[f] / N;
|
|
137
|
+
// Pass 2 — score, keeping only the best `limit`.
|
|
138
|
+
const best = [];
|
|
139
|
+
const worseThanWorst = (score, id) => {
|
|
140
|
+
const w = best[best.length - 1];
|
|
141
|
+
if (score !== w.score)
|
|
142
|
+
return score < w.score;
|
|
143
|
+
return id > w.node.id; // ties: lower id wins, matching the sort below
|
|
144
|
+
};
|
|
145
|
+
for (const node of corpus) {
|
|
146
|
+
const { fields, lengths } = tokenizeFields(node);
|
|
112
147
|
let totalScore = 0;
|
|
113
148
|
const matchedTokens = new Set();
|
|
114
149
|
for (const qt of uniqueQueryTokens) {
|
|
@@ -117,35 +152,36 @@ export function rankCorpus(corpus, query, limit) {
|
|
|
117
152
|
continue;
|
|
118
153
|
// IDF: ln(1 + (N - df + 0.5) / (df + 0.5)) — always positive for df < N
|
|
119
154
|
const idf = Math.log(1 + (N - df + 0.5) / (df + 0.5));
|
|
120
|
-
// Weighted TF across fields (BM25F-style)
|
|
121
155
|
let weightedTf = 0;
|
|
122
156
|
for (const f of FIELDS) {
|
|
123
|
-
const
|
|
124
|
-
const tf = toks.filter((t) => t === qt).length;
|
|
157
|
+
const tf = countOccurrences(fields[f], qt);
|
|
125
158
|
if (tf === 0)
|
|
126
159
|
continue;
|
|
127
160
|
matchedTokens.add(qt);
|
|
128
|
-
const fieldLen =
|
|
161
|
+
const fieldLen = lengths[f];
|
|
129
162
|
const avgLen = avgFieldLen[f] > 0 ? avgFieldLen[f] : 1;
|
|
130
|
-
// BM25 saturation with length normalization
|
|
131
163
|
const saturated = (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (fieldLen / avgLen)));
|
|
132
164
|
weightedTf += WEIGHTS[f] * saturated;
|
|
133
165
|
}
|
|
134
166
|
totalScore += idf * weightedTf;
|
|
135
167
|
}
|
|
136
|
-
if (totalScore
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
168
|
+
if (totalScore <= 0)
|
|
169
|
+
continue;
|
|
170
|
+
// Round to fixed precision to prevent cross-platform FP jitter from
|
|
171
|
+
// reordering ties.
|
|
172
|
+
const score = Math.round(totalScore * SCORE_PRECISION) / SCORE_PRECISION;
|
|
173
|
+
if (best.length >= limit && worseThanWorst(score, node.id))
|
|
174
|
+
continue;
|
|
175
|
+
best.push({ node, score, matched: [...matchedTokens].sort() });
|
|
176
|
+
best.sort((a, b) => {
|
|
177
|
+
const diff = b.score - a.score;
|
|
178
|
+
if (diff !== 0)
|
|
179
|
+
return diff;
|
|
180
|
+
return a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0;
|
|
181
|
+
});
|
|
182
|
+
if (best.length > limit)
|
|
183
|
+
best.pop();
|
|
141
184
|
}
|
|
142
|
-
|
|
143
|
-
scored.sort((a, b) => {
|
|
144
|
-
const diff = b.score - a.score;
|
|
145
|
-
if (diff !== 0)
|
|
146
|
-
return diff;
|
|
147
|
-
return a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0;
|
|
148
|
-
});
|
|
149
|
-
return scored.slice(0, limit);
|
|
185
|
+
return best;
|
|
150
186
|
}
|
|
151
187
|
//# sourceMappingURL=bm25f.js.map
|
package/dist/search/bm25f.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bm25f.js","sourceRoot":"","sources":["../../src/search/bm25f.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,8BAA8B;AAC9B,MAAM,OAAO,GAA2B;IACtC,cAAc,EAAE,EAAE;IAClB,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC;CACb,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,CAAU,CAAC;AAGxF,MAAM,EAAE,GAAG,GAAG,CAAC;AACf,MAAM,CAAC,GAAG,IAAI,CAAC;AACf,MAAM,eAAe,GAAG,GAAG,CAAC,CAAC,4BAA4B;AAEzD;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,CAAS;IAChC,8FAA8F;IAC9F,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACvC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,8BAA8B;QAC9B,iHAAiH;QACjH,8HAA8H;QAC9H,MAAM,QAAQ,GAAG,IAAI;aAClB,OAAO,CAAC,iBAAiB,EAAE,QAAQ,CAAC;aACpC,OAAO,CAAC,uBAAuB,EAAE,QAAQ,CAAC;aAC1C,KAAK,CAAC,IAAI,CAAC,CAAC;QACf,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC/B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8CAA8C;AAC9C,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC;
|
|
1
|
+
{"version":3,"file":"bm25f.js","sourceRoot":"","sources":["../../src/search/bm25f.ts"],"names":[],"mappings":"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"}
|
|
@@ -74,8 +74,13 @@ export interface GraphStore {
|
|
|
74
74
|
findByContentHash?(repoIds: string[], hash: string): Promise<TargetRow[]>;
|
|
75
75
|
/** Raw read-only Cypher (read_cypher tool only). Stores without a Cypher
|
|
76
76
|
* engine throw CypherUnsupportedError. */
|
|
77
|
-
runRead(query: string, params?: Record<string, unknown>,
|
|
77
|
+
runRead(query: string, params?: Record<string, unknown>,
|
|
78
|
+
/** `maxRows` is a request to STOP STREAMING there, not a post-hoc filter.
|
|
79
|
+
* An implementation that cannot stream may return everything, but one
|
|
80
|
+
* that can must not materialise past the cap. */
|
|
81
|
+
opts?: {
|
|
78
82
|
timeoutMs?: number;
|
|
83
|
+
maxRows?: number;
|
|
79
84
|
}): Promise<Record<string, unknown>[]>;
|
|
80
85
|
close(): Promise<void>;
|
|
81
86
|
}
|
|
@@ -13,7 +13,12 @@ import type { TargetRow } from "../profile/types.js";
|
|
|
13
13
|
export declare class JsonlGraphStore implements GraphStore {
|
|
14
14
|
private readonly repoId;
|
|
15
15
|
private graph;
|
|
16
|
-
private
|
|
16
|
+
private graphStamp;
|
|
17
|
+
private overlayStamp;
|
|
18
|
+
/** Per node, the prop values the overlay overwrote — its own undo log.
|
|
19
|
+
* A key absent before the overlay is recorded as `undefined`, so removing a
|
|
20
|
+
* summary removes the props it added instead of leaving them behind. */
|
|
21
|
+
private overlayUndo;
|
|
17
22
|
private readonly repoPath;
|
|
18
23
|
private readonly nodesPath;
|
|
19
24
|
private readonly edgesPath;
|
|
@@ -27,17 +32,49 @@ export declare class JsonlGraphStore implements GraphStore {
|
|
|
27
32
|
* children (guarded, cycle-safe). Each child's repo_id comes from the proxy
|
|
28
33
|
* Repository node's federated_repo_id in the parent's nodes.jsonl. */
|
|
29
34
|
private collectRepos;
|
|
30
|
-
/**
|
|
35
|
+
/** Two inputs, two keys.
|
|
31
36
|
*
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
private
|
|
37
|
+
* The derived JSONL and the authored overlay change independently and cost
|
|
38
|
+
* wildly different amounts to re-read. Keying both together meant every
|
|
39
|
+
* `write_semantic_summary` — which rewrites this machine's sidecar — threw
|
|
40
|
+
* the parsed graph away and re-read nodes.jsonl, edges.jsonl and every
|
|
41
|
+
* shard on the next call. The node had already been updated in place, so
|
|
42
|
+
* that rebuild bought nothing; a summarisation loop of K writes cost K full
|
|
43
|
+
* reloads. Stat calls only, O(shards + sidecars). */
|
|
44
|
+
private graphKey;
|
|
45
|
+
/** The committed shards AND every agent's sidecar.
|
|
46
|
+
*
|
|
47
|
+
* Keying on nodes/edges alone made a `git pull` that brought in only a
|
|
48
|
+
* teammate's shards invisible for the life of the process — the shards are
|
|
49
|
+
* committed and the derived graph is not, so that is the ordinary case.
|
|
50
|
+
* Leaving the sidecars out did the same to a second agent working the same
|
|
51
|
+
* checkout, which the overlay explicitly promises to surface. */
|
|
52
|
+
private overlayKey;
|
|
40
53
|
private ensureFresh;
|
|
54
|
+
/** Write overlay props onto a node, remembering what they displaced.
|
|
55
|
+
*
|
|
56
|
+
* Every in-place summary mutation goes through here — the overlay fold AND
|
|
57
|
+
* writeSummary. A mutation that skipped it would be invisible to the undo
|
|
58
|
+
* pass, and worse, the NEXT fold would record the value it wrote as the
|
|
59
|
+
* pre-overlay original: removing that summary would then restore it. */
|
|
60
|
+
private overlayProps;
|
|
61
|
+
/** Fold the authored summaries onto the parsed graph, in place.
|
|
62
|
+
*
|
|
63
|
+
* The committed shards first, then this machine's sidecars as a strictly
|
|
64
|
+
* newer source, through the same rules the indexer uses. So what the server
|
|
65
|
+
* serves is exactly what the next `index` will write — including which side
|
|
66
|
+
* wins a divergence.
|
|
67
|
+
*
|
|
68
|
+
* The shard overlay only matters between indexes (`index` already grafts
|
|
69
|
+
* them into nodes.jsonl), but that is the ordinary case: a teammate's
|
|
70
|
+
* summaries arrive with a `git pull` long before anyone re-indexes.
|
|
71
|
+
* Sidecars are every agent's, not just this process's, so two agents on one
|
|
72
|
+
* checkout see each other's work.
|
|
73
|
+
*
|
|
74
|
+
* Re-applying is now routine rather than a side effect of a rebuild, so it
|
|
75
|
+
* undoes the previous overlay first: a summary that has gone away must take
|
|
76
|
+
* its props with it. */
|
|
77
|
+
private applyOverlay;
|
|
41
78
|
getNode(repoIds: string[], id: string): Promise<TargetRow | null>;
|
|
42
79
|
findByContentHash(repoIds: string[], hash: string): Promise<TargetRow[]>;
|
|
43
80
|
resolveByPathAndName(repoIds: string[], filePath: string, name: string): Promise<TargetRow[]>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { statSync,
|
|
1
|
+
import { statSync, existsSync } from "node:fs";
|
|
2
|
+
import { readLines } from "./jsonlLines.js";
|
|
2
3
|
import { join, resolve, isAbsolute, sep } from "node:path";
|
|
3
4
|
import { CypherUnsupportedError, } from "./GraphStore.js";
|
|
4
|
-
import {
|
|
5
|
+
import { buildFederatedGraphLines, emptyGraph, } from "./jsonlGraph.js";
|
|
5
6
|
import { loadAllSidecars, sidecarPaths, sidecarPath, upsertSidecar } from "./sidecar.js";
|
|
6
7
|
import { absorbSummarySource, emptyAccumulator, loadSummaryShards, recordSummaryConflicts, summaryShardsStamp, } from "./summaryShards.js";
|
|
7
8
|
function str(v) {
|
|
@@ -73,7 +74,12 @@ const hasLabel = (n, l) => n.labels.includes(l);
|
|
|
73
74
|
export class JsonlGraphStore {
|
|
74
75
|
repoId;
|
|
75
76
|
graph = emptyGraph();
|
|
76
|
-
|
|
77
|
+
graphStamp = "";
|
|
78
|
+
overlayStamp = "";
|
|
79
|
+
/** Per node, the prop values the overlay overwrote — its own undo log.
|
|
80
|
+
* A key absent before the overlay is recorded as `undefined`, so removing a
|
|
81
|
+
* summary removes the props it added instead of leaving them behind. */
|
|
82
|
+
overlayUndo = new Map();
|
|
77
83
|
repoPath;
|
|
78
84
|
nodesPath;
|
|
79
85
|
edgesPath;
|
|
@@ -103,22 +109,21 @@ export class JsonlGraphStore {
|
|
|
103
109
|
seen.add(repoId);
|
|
104
110
|
const nodesPath = join(dir, ".reposkein", "nodes.jsonl");
|
|
105
111
|
const edgesPath = join(dir, ".reposkein", "edges.jsonl");
|
|
106
|
-
|
|
107
|
-
let edgesText = "";
|
|
108
|
-
try {
|
|
109
|
-
if (existsSync(nodesPath))
|
|
110
|
-
nodesText = readFileSync(nodesPath, "utf8");
|
|
111
|
-
if (existsSync(edgesPath))
|
|
112
|
-
edgesText = readFileSync(edgesPath, "utf8");
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
112
|
+
if (!existsSync(nodesPath) && !existsSync(edgesPath))
|
|
115
113
|
return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
// Lazy: nothing is read until the build iterates them, and only one line
|
|
115
|
+
// is live at a time. This used to hold the COMPLETE text of every
|
|
116
|
+
// federated repo simultaneously — a root plus ten children meant the
|
|
117
|
+
// whole federation resident at once, before a single line was parsed.
|
|
118
|
+
repos.push({
|
|
119
|
+
repoId,
|
|
120
|
+
nodes: { [Symbol.iterator]: () => readLines(nodesPath) },
|
|
121
|
+
edges: { [Symbol.iterator]: () => readLines(edgesPath) },
|
|
122
|
+
});
|
|
123
|
+
// Discover children from this repo's Repository proxy nodes. A second
|
|
124
|
+
// streaming pass over the same file, which costs I/O rather than the
|
|
125
|
+
// second full line array the old code built.
|
|
126
|
+
for (const line of readLines(nodesPath)) {
|
|
122
127
|
let obj;
|
|
123
128
|
try {
|
|
124
129
|
obj = JSON.parse(line);
|
|
@@ -141,16 +146,16 @@ export class JsonlGraphStore {
|
|
|
141
146
|
visit(this.repoPath, this.repoId);
|
|
142
147
|
return repos;
|
|
143
148
|
}
|
|
144
|
-
/**
|
|
149
|
+
/** Two inputs, two keys.
|
|
145
150
|
*
|
|
146
|
-
* The
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
151
|
+
* The derived JSONL and the authored overlay change independently and cost
|
|
152
|
+
* wildly different amounts to re-read. Keying both together meant every
|
|
153
|
+
* `write_semantic_summary` — which rewrites this machine's sidecar — threw
|
|
154
|
+
* the parsed graph away and re-read nodes.jsonl, edges.jsonl and every
|
|
155
|
+
* shard on the next call. The node had already been updated in place, so
|
|
156
|
+
* that rebuild bought nothing; a summarisation loop of K writes cost K full
|
|
157
|
+
* reloads. Stat calls only, O(shards + sidecars). */
|
|
158
|
+
graphKey() {
|
|
154
159
|
let m = 0;
|
|
155
160
|
try {
|
|
156
161
|
if (existsSync(this.nodesPath))
|
|
@@ -161,25 +166,93 @@ export class JsonlGraphStore {
|
|
|
161
166
|
catch {
|
|
162
167
|
m = 0;
|
|
163
168
|
}
|
|
164
|
-
return
|
|
169
|
+
return String(m);
|
|
170
|
+
}
|
|
171
|
+
/** The committed shards AND every agent's sidecar.
|
|
172
|
+
*
|
|
173
|
+
* Keying on nodes/edges alone made a `git pull` that brought in only a
|
|
174
|
+
* teammate's shards invisible for the life of the process — the shards are
|
|
175
|
+
* committed and the derived graph is not, so that is the ordinary case.
|
|
176
|
+
* Leaving the sidecars out did the same to a second agent working the same
|
|
177
|
+
* checkout, which the overlay explicitly promises to surface. */
|
|
178
|
+
overlayKey() {
|
|
179
|
+
return summaryShardsStamp(this.repoPath, sidecarPaths);
|
|
165
180
|
}
|
|
166
181
|
ensureFresh() {
|
|
167
|
-
const
|
|
168
|
-
|
|
182
|
+
const graphKey = this.graphKey();
|
|
183
|
+
const overlayKey = this.overlayKey();
|
|
184
|
+
if (graphKey === this.graphStamp && overlayKey === this.overlayStamp)
|
|
169
185
|
return;
|
|
170
|
-
this.
|
|
186
|
+
if (graphKey !== this.graphStamp) {
|
|
187
|
+
this.graphStamp = graphKey;
|
|
188
|
+
// A fresh graph carries no overlay, and the undo log refers to the node
|
|
189
|
+
// objects that were just discarded.
|
|
190
|
+
this.overlayUndo.clear();
|
|
191
|
+
this.overlayStamp = "";
|
|
192
|
+
try {
|
|
193
|
+
this.graph = buildFederatedGraphLines(this.collectRepos());
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
this.graph = emptyGraph();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (overlayKey !== this.overlayStamp) {
|
|
201
|
+
// Only bank the stamp when the overlay actually landed. Banking it first
|
|
202
|
+
// and then throwing would leave the graph with its previous overlay
|
|
203
|
+
// already undone and no new one applied — every summary silently gone
|
|
204
|
+
// until some unrelated change moved the key again.
|
|
205
|
+
this.overlayStamp = this.applyOverlay() ? overlayKey : "";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Write overlay props onto a node, remembering what they displaced.
|
|
209
|
+
*
|
|
210
|
+
* Every in-place summary mutation goes through here — the overlay fold AND
|
|
211
|
+
* writeSummary. A mutation that skipped it would be invisible to the undo
|
|
212
|
+
* pass, and worse, the NEXT fold would record the value it wrote as the
|
|
213
|
+
* pre-overlay original: removing that summary would then restore it. */
|
|
214
|
+
overlayProps(id, n, props) {
|
|
215
|
+
const saved = this.overlayUndo.get(id) ?? {};
|
|
216
|
+
for (const [k, v] of Object.entries(props)) {
|
|
217
|
+
// First writer wins the undo slot: it holds the pristine value, and a
|
|
218
|
+
// later overwrite would replace it with an already-overlaid one.
|
|
219
|
+
if (!Object.prototype.hasOwnProperty.call(saved, k)) {
|
|
220
|
+
saved[k] = Object.prototype.hasOwnProperty.call(n.props, k) ? n.props[k] : undefined;
|
|
221
|
+
}
|
|
222
|
+
n.props[k] = v;
|
|
223
|
+
}
|
|
224
|
+
this.overlayUndo.set(id, saved);
|
|
225
|
+
}
|
|
226
|
+
/** Fold the authored summaries onto the parsed graph, in place.
|
|
227
|
+
*
|
|
228
|
+
* The committed shards first, then this machine's sidecars as a strictly
|
|
229
|
+
* newer source, through the same rules the indexer uses. So what the server
|
|
230
|
+
* serves is exactly what the next `index` will write — including which side
|
|
231
|
+
* wins a divergence.
|
|
232
|
+
*
|
|
233
|
+
* The shard overlay only matters between indexes (`index` already grafts
|
|
234
|
+
* them into nodes.jsonl), but that is the ordinary case: a teammate's
|
|
235
|
+
* summaries arrive with a `git pull` long before anyone re-indexes.
|
|
236
|
+
* Sidecars are every agent's, not just this process's, so two agents on one
|
|
237
|
+
* checkout see each other's work.
|
|
238
|
+
*
|
|
239
|
+
* Re-applying is now routine rather than a side effect of a rebuild, so it
|
|
240
|
+
* undoes the previous overlay first: a summary that has gone away must take
|
|
241
|
+
* its props with it. */
|
|
242
|
+
applyOverlay() {
|
|
171
243
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
for (const [id, saved] of this.overlayUndo) {
|
|
245
|
+
const n = this.graph.byId.get(id);
|
|
246
|
+
if (!n)
|
|
247
|
+
continue;
|
|
248
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
249
|
+
if (v === undefined)
|
|
250
|
+
delete n.props[k];
|
|
251
|
+
else
|
|
252
|
+
n.props[k] = v;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.overlayUndo.clear();
|
|
183
256
|
const shards = loadSummaryShards(this.repoPath);
|
|
184
257
|
const overlay = emptyAccumulator();
|
|
185
258
|
absorbSummarySource(overlay, {
|
|
@@ -199,16 +272,17 @@ export class JsonlGraphStore {
|
|
|
199
272
|
// node passes this the same way a shard record does.
|
|
200
273
|
if (rec.props.summary_of_hash !== n.props.content_hash)
|
|
201
274
|
continue;
|
|
202
|
-
|
|
203
|
-
n.props[k] = v;
|
|
275
|
+
this.overlayProps(id, n, rec.props);
|
|
204
276
|
}
|
|
205
277
|
// Divergence losers — from a merged shard, or from two agents writing the
|
|
206
278
|
// same node — are authored prose. Preserve them for a human rather than
|
|
207
279
|
// discarding them on the read path.
|
|
208
280
|
recordSummaryConflicts(this.repoPath, [...shards.conflicts, ...overlay.conflicts]);
|
|
281
|
+
return true;
|
|
209
282
|
}
|
|
210
283
|
catch {
|
|
211
|
-
|
|
284
|
+
// An unreadable overlay must not cost the caller the graph.
|
|
285
|
+
return false;
|
|
212
286
|
}
|
|
213
287
|
}
|
|
214
288
|
async getNode(repoIds, id) {
|
|
@@ -286,11 +360,16 @@ export class JsonlGraphStore {
|
|
|
286
360
|
const oldSummary = str(n.props.semantic_summary);
|
|
287
361
|
const oldHash = str(n.props.summary_of_hash);
|
|
288
362
|
const stale_replaced = oldSummary !== null && oldHash !== chash;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
363
|
+
// Through overlayProps, not raw assignment: this mutation has to be
|
|
364
|
+
// undoable like any other overlay write, or the next fold would mistake
|
|
365
|
+
// what it wrote for the node's pre-overlay state.
|
|
366
|
+
this.overlayProps(id, n, {
|
|
367
|
+
semantic_summary: fields.summary,
|
|
368
|
+
summary_of_hash: chash,
|
|
369
|
+
summary_model: fields.model,
|
|
370
|
+
summary_at: fields.at,
|
|
371
|
+
summary_by: fields.by,
|
|
372
|
+
});
|
|
294
373
|
upsertSidecar(this.sidecarFile, {
|
|
295
374
|
id,
|
|
296
375
|
semantic_summary: fields.summary,
|