recallx 1.0.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 +205 -0
- package/app/cli/bin/recallx-mcp.js +2 -0
- package/app/cli/bin/recallx.js +8 -0
- package/app/cli/src/cli.js +808 -0
- package/app/cli/src/format.js +242 -0
- package/app/cli/src/http.js +35 -0
- package/app/mcp/api-client.js +101 -0
- package/app/mcp/index.js +128 -0
- package/app/mcp/server.js +786 -0
- package/app/server/app.js +2263 -0
- package/app/server/config.js +27 -0
- package/app/server/db.js +399 -0
- package/app/server/errors.js +17 -0
- package/app/server/governance.js +466 -0
- package/app/server/index.js +26 -0
- package/app/server/inferred-relations.js +247 -0
- package/app/server/observability.js +495 -0
- package/app/server/project-graph.js +199 -0
- package/app/server/relation-scoring.js +59 -0
- package/app/server/repositories.js +2992 -0
- package/app/server/retrieval.js +486 -0
- package/app/server/semantic/chunker.js +85 -0
- package/app/server/semantic/provider.js +124 -0
- package/app/server/semantic/types.js +1 -0
- package/app/server/semantic/vector-store.js +169 -0
- package/app/server/utils.js +43 -0
- package/app/server/workspace-session.js +128 -0
- package/app/server/workspace.js +79 -0
- package/app/shared/contracts.js +268 -0
- package/app/shared/request-runtime.js +30 -0
- package/app/shared/types.js +1 -0
- package/app/shared/version.js +1 -0
- package/dist/renderer/assets/ProjectGraphCanvas-BMvz9DmE.js +312 -0
- package/dist/renderer/assets/index-C2-KXqBO.css +1 -0
- package/dist/renderer/assets/index-CrDu22h7.js +76 -0
- package/dist/renderer/index.html +13 -0
- package/package.json +49 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const AUTO_INFERRED_GENERATORS = [
|
|
2
|
+
"deterministic-tag-overlap",
|
|
3
|
+
"deterministic-body-reference",
|
|
4
|
+
"deterministic-activity-reference",
|
|
5
|
+
"deterministic-project-membership",
|
|
6
|
+
"deterministic-shared-artifact"
|
|
7
|
+
];
|
|
8
|
+
const MAX_CANDIDATES = 200;
|
|
9
|
+
const MAX_INFERRED_PER_NODE = 12;
|
|
10
|
+
const MAX_ACTIVITY_BODIES = 8;
|
|
11
|
+
function normalizeText(value) {
|
|
12
|
+
return (value ?? "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
function normalizeTags(tags) {
|
|
15
|
+
return Array.from(new Set(tags.map((tag) => normalizeText(tag)).filter(Boolean)));
|
|
16
|
+
}
|
|
17
|
+
function escapeRegExp(value) {
|
|
18
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
|
+
}
|
|
20
|
+
function titlePattern(title) {
|
|
21
|
+
const normalized = normalizeText(title);
|
|
22
|
+
if (normalized.length < 5) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return new RegExp(`(^|[^a-z0-9])${escapeRegExp(normalized)}([^a-z0-9]|$)`, "i");
|
|
26
|
+
}
|
|
27
|
+
function mentionsNode(haystacks, candidate) {
|
|
28
|
+
const normalizedHaystacks = haystacks.map(normalizeText).filter(Boolean);
|
|
29
|
+
const idMention = normalizedHaystacks.some((haystack) => haystack.includes(candidate.id.toLowerCase()));
|
|
30
|
+
const candidateTitlePattern = candidate.title ? titlePattern(candidate.title) : null;
|
|
31
|
+
const titleMention = candidateTitlePattern
|
|
32
|
+
? normalizedHaystacks.some((haystack) => candidateTitlePattern.test(haystack))
|
|
33
|
+
: false;
|
|
34
|
+
return { idMention, titleMention };
|
|
35
|
+
}
|
|
36
|
+
function sortPair(left, right) {
|
|
37
|
+
return left.localeCompare(right) <= 0 ? [left, right] : [right, left];
|
|
38
|
+
}
|
|
39
|
+
function buildTagOverlapCandidate(target, candidate, targetTags) {
|
|
40
|
+
const candidateTags = new Set(normalizeTags(candidate.tags));
|
|
41
|
+
const sharedTags = targetTags.filter((tag) => candidateTags.has(tag));
|
|
42
|
+
if (!sharedTags.length) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
|
|
46
|
+
return {
|
|
47
|
+
fromNodeId,
|
|
48
|
+
toNodeId,
|
|
49
|
+
relationType: "related_to",
|
|
50
|
+
generator: "deterministic-tag-overlap",
|
|
51
|
+
baseScore: Math.min(0.62, 0.36 + sharedTags.length * 0.1),
|
|
52
|
+
evidence: {
|
|
53
|
+
sharedTags
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function buildBodyReferenceCandidate(target, candidate) {
|
|
58
|
+
const targetMentionsCandidate = mentionsNode([target.title ?? "", target.body ?? "", target.summary ?? ""], candidate);
|
|
59
|
+
const candidateMentionsTarget = mentionsNode([candidate.title ?? "", candidate.body ?? "", candidate.summary ?? ""], target);
|
|
60
|
+
if (!targetMentionsCandidate.idMention && !targetMentionsCandidate.titleMention && !candidateMentionsTarget.idMention && !candidateMentionsTarget.titleMention) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
|
|
64
|
+
const idMentionCount = Number(targetMentionsCandidate.idMention) + Number(candidateMentionsTarget.idMention);
|
|
65
|
+
const titleMentionCount = Number(targetMentionsCandidate.titleMention) + Number(candidateMentionsTarget.titleMention);
|
|
66
|
+
return {
|
|
67
|
+
fromNodeId,
|
|
68
|
+
toNodeId,
|
|
69
|
+
relationType: "relevant_to",
|
|
70
|
+
generator: "deterministic-body-reference",
|
|
71
|
+
baseScore: Math.min(0.82, 0.52 + idMentionCount * 0.16 + titleMentionCount * 0.1),
|
|
72
|
+
evidence: {
|
|
73
|
+
targetMentionsCandidate,
|
|
74
|
+
candidateMentionsTarget
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildActivityReferenceCandidate(target, candidate, activityBodies) {
|
|
79
|
+
const activityMentionsCandidate = mentionsNode(activityBodies, candidate);
|
|
80
|
+
const mentionCount = Number(activityMentionsCandidate.idMention) * 2 + Number(activityMentionsCandidate.titleMention);
|
|
81
|
+
if (!mentionCount) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
|
|
85
|
+
return {
|
|
86
|
+
fromNodeId,
|
|
87
|
+
toNodeId,
|
|
88
|
+
relationType: "relevant_to",
|
|
89
|
+
generator: "deterministic-activity-reference",
|
|
90
|
+
baseScore: Math.min(0.74, 0.45 + mentionCount * 0.11),
|
|
91
|
+
evidence: {
|
|
92
|
+
targetNodeId: target.id,
|
|
93
|
+
activityMentionsCandidate
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function buildProjectMembershipCandidate(target, candidate, candidateProjectIds, targetProjects) {
|
|
98
|
+
const sharedProjectIds = candidateProjectIds.filter((projectId) => targetProjects.has(projectId));
|
|
99
|
+
if (!sharedProjectIds.length) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
|
|
103
|
+
return {
|
|
104
|
+
fromNodeId,
|
|
105
|
+
toNodeId,
|
|
106
|
+
relationType: "relevant_to",
|
|
107
|
+
generator: "deterministic-project-membership",
|
|
108
|
+
baseScore: Math.min(0.8, 0.58 + sharedProjectIds.length * 0.08),
|
|
109
|
+
evidence: {
|
|
110
|
+
sharedProjectIds
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function buildSharedArtifactCandidate(target, candidate, candidateArtifacts, targetArtifacts) {
|
|
115
|
+
const candidateExact = new Set(candidateArtifacts.exactPaths);
|
|
116
|
+
const candidateBase = new Set(candidateArtifacts.baseNames);
|
|
117
|
+
const sharedExactPaths = targetArtifacts.exactPaths.filter((artifactPath) => candidateExact.has(artifactPath));
|
|
118
|
+
const sharedBaseNames = targetArtifacts.baseNames.filter((baseName) => candidateBase.has(baseName));
|
|
119
|
+
if (!sharedExactPaths.length && !sharedBaseNames.length) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const [fromNodeId, toNodeId] = sortPair(target.id, candidate.id);
|
|
123
|
+
return {
|
|
124
|
+
fromNodeId,
|
|
125
|
+
toNodeId,
|
|
126
|
+
relationType: "related_to",
|
|
127
|
+
generator: "deterministic-shared-artifact",
|
|
128
|
+
baseScore: Math.min(0.76, 0.48 + sharedExactPaths.length * 0.16 + sharedBaseNames.length * 0.06),
|
|
129
|
+
evidence: {
|
|
130
|
+
sharedExactPaths,
|
|
131
|
+
sharedBaseNames
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function collectGeneratedCandidates(repository, target, trigger) {
|
|
136
|
+
const candidateMap = new Map();
|
|
137
|
+
for (const candidate of repository.listInferenceCandidateNodes(target.id, MAX_CANDIDATES)) {
|
|
138
|
+
candidateMap.set(candidate.id, candidate);
|
|
139
|
+
}
|
|
140
|
+
const extraCandidateIds = [
|
|
141
|
+
...repository.listSharedProjectMemberNodeIds(target.id, MAX_CANDIDATES),
|
|
142
|
+
...repository.listNodesSharingArtifactPaths(target.id, MAX_CANDIDATES)
|
|
143
|
+
].filter((candidateId) => !candidateMap.has(candidateId));
|
|
144
|
+
for (const candidate of repository.getNodesByIds(extraCandidateIds).values()) {
|
|
145
|
+
if (candidate.status === "active" || candidate.status === "contested") {
|
|
146
|
+
candidateMap.set(candidate.id, candidate);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const candidates = Array.from(candidateMap.values());
|
|
150
|
+
const projectMembershipsByNodeId = repository.listProjectMembershipIdsByNodeIds([target.id, ...candidates.map((candidate) => candidate.id)]);
|
|
151
|
+
const artifactKeysByNodeId = repository.listArtifactKeysByNodeIds([target.id, ...candidates.map((candidate) => candidate.id)]);
|
|
152
|
+
const targetContext = {
|
|
153
|
+
normalizedTags: normalizeTags(target.tags),
|
|
154
|
+
projectIds: new Set(projectMembershipsByNodeId.get(target.id) ?? []),
|
|
155
|
+
artifactKeys: artifactKeysByNodeId.get(target.id) ?? { exactPaths: [], baseNames: [] }
|
|
156
|
+
};
|
|
157
|
+
const activityBodies = trigger === "activity-append" || trigger === "reindex"
|
|
158
|
+
? repository
|
|
159
|
+
.listNodeActivities(target.id, MAX_ACTIVITY_BODIES)
|
|
160
|
+
.map((activity) => activity.body ?? "")
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
: [];
|
|
163
|
+
return candidates.flatMap((candidate) => {
|
|
164
|
+
const generated = [];
|
|
165
|
+
const tagOverlapCandidate = buildTagOverlapCandidate(target, candidate, targetContext.normalizedTags);
|
|
166
|
+
if (tagOverlapCandidate) {
|
|
167
|
+
generated.push(tagOverlapCandidate);
|
|
168
|
+
}
|
|
169
|
+
const bodyReferenceCandidate = buildBodyReferenceCandidate(target, candidate);
|
|
170
|
+
if (bodyReferenceCandidate) {
|
|
171
|
+
generated.push(bodyReferenceCandidate);
|
|
172
|
+
}
|
|
173
|
+
if (activityBodies.length) {
|
|
174
|
+
const activityReferenceCandidate = buildActivityReferenceCandidate(target, candidate, activityBodies);
|
|
175
|
+
if (activityReferenceCandidate) {
|
|
176
|
+
generated.push(activityReferenceCandidate);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const projectMembershipCandidate = buildProjectMembershipCandidate(target, candidate, projectMembershipsByNodeId.get(candidate.id) ?? [], targetContext.projectIds);
|
|
180
|
+
if (projectMembershipCandidate) {
|
|
181
|
+
generated.push(projectMembershipCandidate);
|
|
182
|
+
}
|
|
183
|
+
const sharedArtifactCandidate = buildSharedArtifactCandidate(target, candidate, artifactKeysByNodeId.get(candidate.id) ?? { exactPaths: [], baseNames: [] }, targetContext.artifactKeys);
|
|
184
|
+
if (sharedArtifactCandidate) {
|
|
185
|
+
generated.push(sharedArtifactCandidate);
|
|
186
|
+
}
|
|
187
|
+
return generated;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export function refreshAutomaticInferredRelationsForNode(repository, nodeId, trigger) {
|
|
191
|
+
const target = repository.getNode(nodeId);
|
|
192
|
+
if (target.status === "archived") {
|
|
193
|
+
const expiredCount = repository.expireAutoInferredRelationsForNode(nodeId, [...AUTO_INFERRED_GENERATORS]);
|
|
194
|
+
return { upsertedCount: 0, expiredCount, relationIds: [] };
|
|
195
|
+
}
|
|
196
|
+
const deduped = new Map();
|
|
197
|
+
for (const candidate of collectGeneratedCandidates(repository, target, trigger)) {
|
|
198
|
+
const key = [candidate.fromNodeId, candidate.toNodeId, candidate.relationType, candidate.generator].join(":");
|
|
199
|
+
const existing = deduped.get(key);
|
|
200
|
+
if (!existing || candidate.baseScore > existing.baseScore) {
|
|
201
|
+
deduped.set(key, candidate);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const relationIds = Array.from(deduped.values())
|
|
205
|
+
.sort((left, right) => right.baseScore - left.baseScore)
|
|
206
|
+
.slice(0, MAX_INFERRED_PER_NODE)
|
|
207
|
+
.map((candidate) => repository.upsertInferredRelation({
|
|
208
|
+
fromNodeId: candidate.fromNodeId,
|
|
209
|
+
toNodeId: candidate.toNodeId,
|
|
210
|
+
relationType: candidate.relationType,
|
|
211
|
+
baseScore: candidate.baseScore,
|
|
212
|
+
usageScore: 0,
|
|
213
|
+
finalScore: candidate.baseScore,
|
|
214
|
+
status: "active",
|
|
215
|
+
generator: candidate.generator,
|
|
216
|
+
evidence: candidate.evidence,
|
|
217
|
+
metadata: {
|
|
218
|
+
trigger
|
|
219
|
+
}
|
|
220
|
+
}).id);
|
|
221
|
+
const expiredCount = repository.expireAutoInferredRelationsForNode(nodeId, [...AUTO_INFERRED_GENERATORS], relationIds);
|
|
222
|
+
return {
|
|
223
|
+
upsertedCount: relationIds.length,
|
|
224
|
+
expiredCount,
|
|
225
|
+
relationIds
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export function reindexAutomaticInferredRelations(repository, input) {
|
|
229
|
+
const targetNodeIds = repository.listInferenceTargetNodeIds(input?.limit ?? 250);
|
|
230
|
+
let upsertedCount = 0;
|
|
231
|
+
let expiredCount = 0;
|
|
232
|
+
const relationIds = new Set();
|
|
233
|
+
for (const nodeId of targetNodeIds) {
|
|
234
|
+
const result = refreshAutomaticInferredRelationsForNode(repository, nodeId, "reindex");
|
|
235
|
+
upsertedCount += result.upsertedCount;
|
|
236
|
+
expiredCount += result.expiredCount;
|
|
237
|
+
for (const relationId of result.relationIds) {
|
|
238
|
+
relationIds.add(relationId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
processedNodes: targetNodeIds.length,
|
|
243
|
+
upsertedCount,
|
|
244
|
+
expiredCount,
|
|
245
|
+
relationIds: Array.from(relationIds)
|
|
246
|
+
};
|
|
247
|
+
}
|