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,466 @@
|
|
|
1
|
+
import { AppError } from "./errors.js";
|
|
2
|
+
import { countTokensApprox, nowIso } from "./utils.js";
|
|
3
|
+
const relaxedShortFormNodeTypes = new Set(["reference", "question", "conversation"]);
|
|
4
|
+
function clampConfidence(value) {
|
|
5
|
+
return Math.min(Math.max(value, 0), 1);
|
|
6
|
+
}
|
|
7
|
+
function readTrustedSourceToolNames(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
return value
|
|
13
|
+
.split(",")
|
|
14
|
+
.map((item) => item.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
function isTrustedAgentSource(toolName, policy) {
|
|
20
|
+
return policy.trustedSourceToolNames.includes(toolName);
|
|
21
|
+
}
|
|
22
|
+
function feedbackConfidenceBonus(summary) {
|
|
23
|
+
if (!summary) {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
return Math.min(Math.max(summary.totalDelta, -2), 2) * 0.12;
|
|
27
|
+
}
|
|
28
|
+
function stabilityBonus(timestamp) {
|
|
29
|
+
const ageMs = Date.now() - new Date(timestamp).getTime();
|
|
30
|
+
if (ageMs >= 24 * 60 * 60 * 1000)
|
|
31
|
+
return 0.06;
|
|
32
|
+
if (ageMs >= 60 * 60 * 1000)
|
|
33
|
+
return 0.03;
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
function chooseNodeHealthyThreshold(node) {
|
|
37
|
+
if (node.canonicality === "suggested") {
|
|
38
|
+
return node.type === "decision" ? 0.78 : 0.72;
|
|
39
|
+
}
|
|
40
|
+
if (node.canonicality === "canonical") {
|
|
41
|
+
return 0.55;
|
|
42
|
+
}
|
|
43
|
+
return 0.35;
|
|
44
|
+
}
|
|
45
|
+
function chooseRelationActiveThreshold(_relation) {
|
|
46
|
+
return 0.72;
|
|
47
|
+
}
|
|
48
|
+
function chooseNodeBaseConfidence(node, policy) {
|
|
49
|
+
switch (node.sourceType) {
|
|
50
|
+
case "human":
|
|
51
|
+
return 0.95;
|
|
52
|
+
case "import":
|
|
53
|
+
return 0.84;
|
|
54
|
+
case "integration":
|
|
55
|
+
return 0.72;
|
|
56
|
+
case "system":
|
|
57
|
+
return 0.7;
|
|
58
|
+
case "agent":
|
|
59
|
+
return isTrustedAgentSource(node.sourceLabel ?? "", policy) ? 0.62 : 0.48;
|
|
60
|
+
default:
|
|
61
|
+
return 0.45;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function chooseRelationBaseConfidence(relation, policy) {
|
|
65
|
+
switch (relation.sourceType) {
|
|
66
|
+
case "human":
|
|
67
|
+
return 0.78;
|
|
68
|
+
case "import":
|
|
69
|
+
case "integration":
|
|
70
|
+
case "system":
|
|
71
|
+
return 0.68;
|
|
72
|
+
case "agent":
|
|
73
|
+
return isTrustedAgentSource(relation.sourceLabel ?? "", policy) ? 0.62 : 0.42;
|
|
74
|
+
default:
|
|
75
|
+
return 0.4;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function buildEventType(previousState, nextState, changedToCanonical, changedToRejected) {
|
|
79
|
+
if (!previousState) {
|
|
80
|
+
return "evaluated";
|
|
81
|
+
}
|
|
82
|
+
if (changedToCanonical) {
|
|
83
|
+
return "promoted";
|
|
84
|
+
}
|
|
85
|
+
if (nextState === "contested" && previousState.state !== "contested") {
|
|
86
|
+
return "contested";
|
|
87
|
+
}
|
|
88
|
+
if (changedToRejected) {
|
|
89
|
+
return "demoted";
|
|
90
|
+
}
|
|
91
|
+
return "evaluated";
|
|
92
|
+
}
|
|
93
|
+
export function resolveGovernancePolicy(settings) {
|
|
94
|
+
return {
|
|
95
|
+
autoApproveLowRisk: typeof settings?.["review.autoApproveLowRisk"] === "boolean"
|
|
96
|
+
? Boolean(settings["review.autoApproveLowRisk"])
|
|
97
|
+
: true,
|
|
98
|
+
trustedSourceToolNames: readTrustedSourceToolNames(settings?.["review.trustedSourceToolNames"])
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function computeNodeTokenCount(input) {
|
|
102
|
+
const combined = [input.title, input.summary, input.body]
|
|
103
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
return countTokensApprox(combined);
|
|
106
|
+
}
|
|
107
|
+
function hasDurableNodeSignals(input) {
|
|
108
|
+
return Boolean(input.metadata.reusable ||
|
|
109
|
+
input.metadata.durable ||
|
|
110
|
+
input.metadata.promoteCandidate ||
|
|
111
|
+
(typeof input.summary === "string" && input.summary.trim().length > 0));
|
|
112
|
+
}
|
|
113
|
+
export function isShortLogLikeAgentNodeInput(input) {
|
|
114
|
+
return (input.source.actorType === "agent" &&
|
|
115
|
+
input.type !== "decision" &&
|
|
116
|
+
!hasDurableNodeSignals(input) &&
|
|
117
|
+
!relaxedShortFormNodeTypes.has(input.type) &&
|
|
118
|
+
computeNodeTokenCount(input) <= 300);
|
|
119
|
+
}
|
|
120
|
+
export function resolveNodeGovernance(input, policy = resolveGovernancePolicy()) {
|
|
121
|
+
if (input.source.actorType === "human") {
|
|
122
|
+
return {
|
|
123
|
+
canonicality: input.canonicality ?? "canonical",
|
|
124
|
+
status: input.status ?? "active",
|
|
125
|
+
reason: "Human-authored nodes land canonical by default."
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (input.source.actorType === "import") {
|
|
129
|
+
return {
|
|
130
|
+
canonicality: "imported",
|
|
131
|
+
status: input.status ?? "active",
|
|
132
|
+
reason: "Imported material stays imported."
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const tokenCount = computeNodeTokenCount(input);
|
|
136
|
+
const reusable = hasDurableNodeSignals(input);
|
|
137
|
+
const trustedAgentSource = input.source.actorType === "agent" ? isTrustedAgentSource(input.source.toolName, policy) : false;
|
|
138
|
+
if (isShortLogLikeAgentNodeInput(input)) {
|
|
139
|
+
throw new AppError(403, "FORBIDDEN", "Short log-like agent output must be appended as activity, not stored as a durable node.", {
|
|
140
|
+
tokenCount,
|
|
141
|
+
recommendation: "Use POST /api/v1/capture with mode=auto or mode=activity.",
|
|
142
|
+
suggestedMode: "activity",
|
|
143
|
+
suggestedTarget: "workspace-inbox"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (input.type === "decision") {
|
|
147
|
+
return {
|
|
148
|
+
canonicality: "suggested",
|
|
149
|
+
status: "active",
|
|
150
|
+
reason: trustedAgentSource
|
|
151
|
+
? "Trusted agent-authored decisions start suggested and can auto-promote."
|
|
152
|
+
: "Agent-authored decisions start suggested and await automatic confidence promotion."
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (input.source.actorType === "agent" && reusable) {
|
|
156
|
+
return {
|
|
157
|
+
canonicality: "suggested",
|
|
158
|
+
status: "active",
|
|
159
|
+
reason: "Reusable agent-authored knowledge starts suggested and active."
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
canonicality: "appended",
|
|
164
|
+
status: "active",
|
|
165
|
+
reason: trustedAgentSource
|
|
166
|
+
? "Trusted or low-risk agent-authored nodes land append-first."
|
|
167
|
+
: "Low-risk agent-authored nodes land append-first."
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function resolveRelationStatus(input, _policy = resolveGovernancePolicy()) {
|
|
171
|
+
if (input.source.actorType === "agent") {
|
|
172
|
+
return {
|
|
173
|
+
status: "suggested",
|
|
174
|
+
reason: "Agent-authored relations start suggested and rely on automatic governance promotion."
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
status: input.status ?? "active",
|
|
179
|
+
reason: "Human or imported relations land active unless a status is explicitly provided."
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function shouldPromoteActivitySummary(input) {
|
|
183
|
+
const tokenCount = countTokensApprox(input.body);
|
|
184
|
+
const durable = Boolean(input.metadata.reusable || input.metadata.durable || input.metadata.promoteCandidate);
|
|
185
|
+
return input.source.actorType === "agent" && tokenCount > 300 && durable;
|
|
186
|
+
}
|
|
187
|
+
export function maybeCreatePromotionCandidate(repository, input) {
|
|
188
|
+
if (!shouldPromoteActivitySummary(input)) {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
const target = repository.getNode(input.targetNodeId);
|
|
192
|
+
const suggested = repository.createNode({
|
|
193
|
+
type: input.metadata.suggestedType === "reference" ? "reference" : "note",
|
|
194
|
+
title: typeof input.metadata.title === "string" ? input.metadata.title : `${target.title ?? "Untitled"} follow-up`,
|
|
195
|
+
body: input.body,
|
|
196
|
+
summary: typeof input.metadata.summary === "string" ? input.metadata.summary : undefined,
|
|
197
|
+
tags: Array.isArray(input.metadata.tags) ? input.metadata.tags : target.tags,
|
|
198
|
+
canonicality: "suggested",
|
|
199
|
+
status: "active",
|
|
200
|
+
resolvedCanonicality: "suggested",
|
|
201
|
+
resolvedStatus: "active",
|
|
202
|
+
source: input.source,
|
|
203
|
+
metadata: {
|
|
204
|
+
...input.metadata,
|
|
205
|
+
derivedFromActivity: true,
|
|
206
|
+
targetNodeId: input.targetNodeId
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
repository.recordProvenance({
|
|
210
|
+
entityType: "node",
|
|
211
|
+
entityId: suggested.id,
|
|
212
|
+
operationType: "create",
|
|
213
|
+
source: input.source,
|
|
214
|
+
metadata: {
|
|
215
|
+
rule: "activity_to_suggested_promotion"
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return { suggestedNodeId: suggested.id };
|
|
219
|
+
}
|
|
220
|
+
function evaluateNodeGovernance(repository, node, policy, feedback = repository.getSearchFeedbackSummaries("node", [node.id]).get(node.id), previousState = repository.getGovernanceStateNullable("node", node.id)) {
|
|
221
|
+
const contradictionCount = repository.countContradictionRelations(node.id);
|
|
222
|
+
const reusable = Boolean(node.metadata.reusable || node.metadata.durable || node.metadata.promoteCandidate);
|
|
223
|
+
let confidence = chooseNodeBaseConfidence(node, policy);
|
|
224
|
+
const reasons = [`source:${node.sourceType ?? "unknown"}`];
|
|
225
|
+
if (node.canonicality === "canonical")
|
|
226
|
+
confidence += 0.12;
|
|
227
|
+
if (node.canonicality === "appended")
|
|
228
|
+
confidence += 0.05;
|
|
229
|
+
if (node.canonicality === "suggested")
|
|
230
|
+
confidence += 0.02;
|
|
231
|
+
if (reusable) {
|
|
232
|
+
confidence += 0.08;
|
|
233
|
+
reasons.push("durable");
|
|
234
|
+
}
|
|
235
|
+
if (node.type === "decision") {
|
|
236
|
+
confidence += 0.06;
|
|
237
|
+
reasons.push("decision");
|
|
238
|
+
}
|
|
239
|
+
confidence += stabilityBonus(node.updatedAt);
|
|
240
|
+
confidence += feedbackConfidenceBonus(feedback);
|
|
241
|
+
if (feedback?.eventCount) {
|
|
242
|
+
reasons.push(`feedback:${feedback.totalDelta.toFixed(2)}`);
|
|
243
|
+
}
|
|
244
|
+
if (contradictionCount) {
|
|
245
|
+
confidence -= Math.min(0.5, contradictionCount * 0.35);
|
|
246
|
+
reasons.push(`contradictions:${contradictionCount}`);
|
|
247
|
+
}
|
|
248
|
+
const contested = contradictionCount > 0 ||
|
|
249
|
+
(feedback?.notUsefulCount ?? 0) >= 2 ||
|
|
250
|
+
(feedback?.totalDelta ?? 0) <= -1;
|
|
251
|
+
const healthyThreshold = chooseNodeHealthyThreshold(node);
|
|
252
|
+
const canPromote = node.canonicality === "suggested" && !contested && confidence >= healthyThreshold;
|
|
253
|
+
const nextCanonicality = canPromote ? "canonical" : node.canonicality;
|
|
254
|
+
const nextStatus = contested ? "contested" : node.status === "contested" ? "active" : node.status === "archived" ? "archived" : "active";
|
|
255
|
+
const nextState = contested ? "contested" : confidence >= healthyThreshold ? "healthy" : "low_confidence";
|
|
256
|
+
return {
|
|
257
|
+
entityType: "node",
|
|
258
|
+
entityId: node.id,
|
|
259
|
+
state: nextState,
|
|
260
|
+
confidence: clampConfidence(confidence),
|
|
261
|
+
reasons,
|
|
262
|
+
eventType: buildEventType(previousState, nextState, canPromote, false),
|
|
263
|
+
nextNodeStatus: nextStatus,
|
|
264
|
+
nextCanonicality,
|
|
265
|
+
metadata: {
|
|
266
|
+
contradictionCount,
|
|
267
|
+
feedbackDelta: feedback?.totalDelta ?? 0,
|
|
268
|
+
feedbackCount: feedback?.eventCount ?? 0
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function evaluateRelationGovernance(repository, relation, policy, usage = repository.getRelationUsageSummaries([relation.id]).get(relation.id), previousState = repository.getGovernanceStateNullable("relation", relation.id)) {
|
|
273
|
+
let confidence = chooseRelationBaseConfidence(relation, policy);
|
|
274
|
+
const reasons = [`source:${relation.sourceType ?? "unknown"}`];
|
|
275
|
+
if (relation.status === "active") {
|
|
276
|
+
confidence += 0.08;
|
|
277
|
+
}
|
|
278
|
+
if (usage) {
|
|
279
|
+
confidence += Math.min(Math.max(usage.totalDelta, -2), 2) * 0.15;
|
|
280
|
+
reasons.push(`usage:${usage.totalDelta.toFixed(2)}`);
|
|
281
|
+
}
|
|
282
|
+
confidence = clampConfidence(confidence);
|
|
283
|
+
const hardReject = (usage?.eventCount ?? 0) >= 2 && (usage?.totalDelta ?? 0) <= -1.25;
|
|
284
|
+
const contested = (usage?.totalDelta ?? 0) <= -0.75;
|
|
285
|
+
const activeThreshold = chooseRelationActiveThreshold(relation);
|
|
286
|
+
const nextRelationStatus = hardReject
|
|
287
|
+
? "rejected"
|
|
288
|
+
: confidence >= activeThreshold
|
|
289
|
+
? "active"
|
|
290
|
+
: "suggested";
|
|
291
|
+
const nextState = contested ? "contested" : confidence >= activeThreshold ? "healthy" : "low_confidence";
|
|
292
|
+
return {
|
|
293
|
+
entityType: "relation",
|
|
294
|
+
entityId: relation.id,
|
|
295
|
+
state: nextState,
|
|
296
|
+
confidence,
|
|
297
|
+
reasons,
|
|
298
|
+
eventType: buildEventType(previousState, nextState, false, nextRelationStatus === "rejected"),
|
|
299
|
+
nextRelationStatus,
|
|
300
|
+
metadata: {
|
|
301
|
+
usageDelta: usage?.totalDelta ?? 0,
|
|
302
|
+
usageCount: usage?.eventCount ?? 0
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function persistGovernanceEvaluation(repository, evaluation, options) {
|
|
307
|
+
const currentState = options?.currentState ?? repository.getGovernanceStateNullable(evaluation.entityType, evaluation.entityId);
|
|
308
|
+
const beforeNode = options?.beforeNode ?? (evaluation.entityType === "node" ? repository.getNode(evaluation.entityId) : null);
|
|
309
|
+
const beforeRelation = options?.beforeRelation ?? (evaluation.entityType === "relation" ? repository.getRelation(evaluation.entityId) : null);
|
|
310
|
+
if (evaluation.entityType === "node") {
|
|
311
|
+
if (evaluation.nextCanonicality && beforeNode && beforeNode.canonicality !== evaluation.nextCanonicality) {
|
|
312
|
+
repository.setNodeCanonicality(evaluation.entityId, evaluation.nextCanonicality);
|
|
313
|
+
}
|
|
314
|
+
if (evaluation.nextNodeStatus && beforeNode && beforeNode.status !== evaluation.nextNodeStatus) {
|
|
315
|
+
repository.updateNode(evaluation.entityId, { status: evaluation.nextNodeStatus });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (evaluation.entityType === "relation" && evaluation.nextRelationStatus && beforeRelation && beforeRelation.status !== evaluation.nextRelationStatus) {
|
|
319
|
+
repository.updateRelationStatus(evaluation.entityId, evaluation.nextRelationStatus);
|
|
320
|
+
}
|
|
321
|
+
const state = repository.upsertGovernanceState({
|
|
322
|
+
entityType: evaluation.entityType,
|
|
323
|
+
entityId: evaluation.entityId,
|
|
324
|
+
state: evaluation.state,
|
|
325
|
+
confidence: evaluation.confidence,
|
|
326
|
+
reasons: evaluation.reasons,
|
|
327
|
+
lastEvaluatedAt: nowIso(),
|
|
328
|
+
metadata: evaluation.metadata,
|
|
329
|
+
previousState: currentState
|
|
330
|
+
});
|
|
331
|
+
repository.appendGovernanceEvent({
|
|
332
|
+
entityType: evaluation.entityType,
|
|
333
|
+
entityId: evaluation.entityId,
|
|
334
|
+
eventType: evaluation.eventType,
|
|
335
|
+
previousState: currentState?.state ?? null,
|
|
336
|
+
nextState: evaluation.state,
|
|
337
|
+
confidence: evaluation.confidence,
|
|
338
|
+
reason: evaluation.reasons.join(", "),
|
|
339
|
+
metadata: {
|
|
340
|
+
...evaluation.metadata,
|
|
341
|
+
nextCanonicality: evaluation.nextCanonicality ?? null,
|
|
342
|
+
nextNodeStatus: evaluation.nextNodeStatus ?? null,
|
|
343
|
+
nextRelationStatus: evaluation.nextRelationStatus ?? null
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return state;
|
|
347
|
+
}
|
|
348
|
+
export function recomputeAutomaticGovernance(repository, input, policy = resolveGovernancePolicy(repository.getSettings(["review.autoApproveLowRisk", "review.trustedSourceToolNames"]))) {
|
|
349
|
+
const targets = repository.recomputeGovernanceTargets(input);
|
|
350
|
+
const items = [];
|
|
351
|
+
let promotedCount = 0;
|
|
352
|
+
let contestedCount = 0;
|
|
353
|
+
const updatedNodes = new Map();
|
|
354
|
+
const nodes = targets.nodeIds.map((nodeId) => repository.getNode(nodeId));
|
|
355
|
+
const nodeFeedback = repository.getSearchFeedbackSummaries("node", nodes.map((node) => node.id));
|
|
356
|
+
const nodeStates = new Map(nodes.map((node) => [node.id, repository.getGovernanceStateNullable("node", node.id)]));
|
|
357
|
+
const relations = targets.relationIds.map((relationId) => repository.getRelation(relationId));
|
|
358
|
+
const relationUsage = repository.getRelationUsageSummaries(relations.map((relation) => relation.id));
|
|
359
|
+
const relationStates = new Map(relations.map((relation) => [relation.id, repository.getGovernanceStateNullable("relation", relation.id)]));
|
|
360
|
+
for (const node of nodes) {
|
|
361
|
+
const evaluation = evaluateNodeGovernance(repository, node, policy, nodeFeedback.get(node.id), nodeStates.get(node.id) ?? null);
|
|
362
|
+
if (evaluation.nextCanonicality === "canonical" && node.canonicality !== "canonical") {
|
|
363
|
+
promotedCount += 1;
|
|
364
|
+
}
|
|
365
|
+
if (evaluation.state === "contested") {
|
|
366
|
+
contestedCount += 1;
|
|
367
|
+
}
|
|
368
|
+
items.push(persistGovernanceEvaluation(repository, evaluation, {
|
|
369
|
+
currentState: nodeStates.get(node.id) ?? null,
|
|
370
|
+
beforeNode: node
|
|
371
|
+
}));
|
|
372
|
+
updatedNodes.set(node.id, {
|
|
373
|
+
...node,
|
|
374
|
+
status: evaluation.nextNodeStatus ?? node.status,
|
|
375
|
+
canonicality: evaluation.nextCanonicality ?? node.canonicality
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
for (const relation of relations) {
|
|
379
|
+
const evaluation = evaluateRelationGovernance(repository, relation, policy, relationUsage.get(relation.id), relationStates.get(relation.id) ?? null);
|
|
380
|
+
if (evaluation.state === "contested") {
|
|
381
|
+
contestedCount += 1;
|
|
382
|
+
}
|
|
383
|
+
items.push(persistGovernanceEvaluation(repository, evaluation, {
|
|
384
|
+
currentState: relationStates.get(relation.id) ?? null,
|
|
385
|
+
beforeRelation: relation
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
updatedCount: items.length,
|
|
390
|
+
promotedCount,
|
|
391
|
+
contestedCount,
|
|
392
|
+
items,
|
|
393
|
+
updatedNodes
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
export function bootstrapAutomaticGovernance(repository) {
|
|
397
|
+
const legacyReviewItems = repository.listLegacyReviewItems();
|
|
398
|
+
if (legacyReviewItems.length === 0) {
|
|
399
|
+
return {
|
|
400
|
+
updatedCount: 0,
|
|
401
|
+
promotedCount: 0,
|
|
402
|
+
contestedCount: 0,
|
|
403
|
+
items: []
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const migratedNodeIds = new Set();
|
|
407
|
+
const migratedRelationIds = new Set();
|
|
408
|
+
for (const item of legacyReviewItems) {
|
|
409
|
+
if (item.entityType !== "node" && item.entityType !== "relation") {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const state = item.status === "pending" ? "contested" : "low_confidence";
|
|
413
|
+
const current = repository.getGovernanceStateNullable(item.entityType, item.entityId);
|
|
414
|
+
repository.upsertGovernanceState({
|
|
415
|
+
entityType: item.entityType,
|
|
416
|
+
entityId: item.entityId,
|
|
417
|
+
state,
|
|
418
|
+
confidence: item.status === "pending" ? 0.2 : 0.4,
|
|
419
|
+
reasons: [`legacy review migrated from ${item.status}`],
|
|
420
|
+
metadata: {
|
|
421
|
+
legacyReviewId: item.id,
|
|
422
|
+
legacyReviewType: item.reviewType
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
repository.appendGovernanceEvent({
|
|
426
|
+
entityType: item.entityType,
|
|
427
|
+
entityId: item.entityId,
|
|
428
|
+
eventType: "migrated",
|
|
429
|
+
previousState: current?.state ?? null,
|
|
430
|
+
nextState: state,
|
|
431
|
+
confidence: item.status === "pending" ? 0.2 : 0.4,
|
|
432
|
+
reason: `Migrated legacy review item ${item.id}`,
|
|
433
|
+
metadata: {
|
|
434
|
+
legacyReviewStatus: item.status,
|
|
435
|
+
legacyReviewType: item.reviewType
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (item.entityType === "node") {
|
|
439
|
+
migratedNodeIds.add(item.entityId);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
migratedRelationIds.add(item.entityId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
repository.clearLegacyReviewQueue();
|
|
446
|
+
const nodeResult = migratedNodeIds.size > 0
|
|
447
|
+
? recomputeAutomaticGovernance(repository, {
|
|
448
|
+
entityType: "node",
|
|
449
|
+
entityIds: Array.from(migratedNodeIds),
|
|
450
|
+
limit: migratedNodeIds.size
|
|
451
|
+
})
|
|
452
|
+
: { updatedCount: 0, promotedCount: 0, contestedCount: 0, items: [] };
|
|
453
|
+
const relationResult = migratedRelationIds.size > 0
|
|
454
|
+
? recomputeAutomaticGovernance(repository, {
|
|
455
|
+
entityType: "relation",
|
|
456
|
+
entityIds: Array.from(migratedRelationIds),
|
|
457
|
+
limit: migratedRelationIds.size
|
|
458
|
+
})
|
|
459
|
+
: { updatedCount: 0, promotedCount: 0, contestedCount: 0, items: [] };
|
|
460
|
+
return {
|
|
461
|
+
updatedCount: nodeResult.updatedCount + relationResult.updatedCount,
|
|
462
|
+
promotedCount: nodeResult.promotedCount + relationResult.promotedCount,
|
|
463
|
+
contestedCount: nodeResult.contestedCount + relationResult.contestedCount,
|
|
464
|
+
items: [...nodeResult.items, ...relationResult.items]
|
|
465
|
+
};
|
|
466
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { createServerConfig, ensureApiToken } from "./config.js";
|
|
3
|
+
import { createRecallXApp, resolveRendererDistDir } from "./app.js";
|
|
4
|
+
import { resolveWorkspaceRoot } from "./workspace.js";
|
|
5
|
+
import { WorkspaceSessionManager } from "./workspace-session.js";
|
|
6
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
7
|
+
const config = createServerConfig(workspaceRoot);
|
|
8
|
+
const apiToken = ensureApiToken(config);
|
|
9
|
+
const workspaceSessionManager = new WorkspaceSessionManager(config, workspaceRoot, config.apiToken ? "bearer" : "optional");
|
|
10
|
+
const app = createRecallXApp({
|
|
11
|
+
workspaceSessionManager,
|
|
12
|
+
apiToken: config.apiToken ? apiToken : null
|
|
13
|
+
});
|
|
14
|
+
createServer(app).listen(config.port, config.bindAddress, () => {
|
|
15
|
+
console.log(`RecallX API listening on http://${config.bindAddress}:${config.port}`);
|
|
16
|
+
if (resolveRendererDistDir()) {
|
|
17
|
+
console.log(`RecallX UI available at http://${config.bindAddress}:${config.port}/`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log("Renderer bundle: not installed (headless mode)");
|
|
21
|
+
}
|
|
22
|
+
console.log(`Workspace root: ${workspaceSessionManager.getCurrent().workspaceRoot}`);
|
|
23
|
+
if (!config.apiToken) {
|
|
24
|
+
console.log("Auth mode: optional (set RECALLX_API_TOKEN to enforce bearer auth)");
|
|
25
|
+
}
|
|
26
|
+
});
|