recallx 1.1.0 → 1.2.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 +33 -1
- package/app/cli/src/cli.js +32 -1
- package/app/cli/src/format.js +14 -0
- package/app/mcp/server.js +248 -127
- package/app/server/app.js +412 -4
- package/app/server/config.js +4 -2
- package/app/server/index.js +12 -1
- package/app/server/project-graph.js +13 -6
- package/app/server/repositories.js +120 -8
- package/app/server/sqlite-errors.js +10 -0
- package/app/server/workspace-import-helpers.js +161 -0
- package/app/server/workspace-import.js +572 -0
- package/app/server/workspace-ops.js +249 -0
- package/app/server/workspace-session.js +118 -6
- package/app/shared/contracts.js +41 -0
- package/app/shared/version.js +1 -1
- package/dist/renderer/assets/{ProjectGraphCanvas-WP0YEOpB.js → ProjectGraphCanvas-B9-L83dL.js} +1 -1
- package/dist/renderer/assets/index-CNeaY_5l.js +69 -0
- package/dist/renderer/assets/index-Dz33nPCb.css +1 -0
- package/dist/renderer/index.html +2 -2
- package/package.json +1 -1
- package/dist/renderer/assets/index-5rwy6MBF.js +0 -69
- package/dist/renderer/assets/index-C2-KXqBO.css +0 -1
package/app/server/app.js
CHANGED
|
@@ -4,13 +4,14 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import cors from "cors";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import mime from "mime-types";
|
|
7
|
-
import { activitySearchSchema, appendActivitySchema, appendRelationUsageEventSchema, appendSearchFeedbackSchema, attachArtifactSchema, buildContextBundleSchema, captureMemorySchema, createWorkspaceSchema, createNodeSchema, createNodesSchema, createRelationSchema, governanceIssuesQuerySchema, nodeSearchSchema, openWorkspaceSchema, reindexInferredRelationsSchema, recomputeGovernanceSchema, recomputeInferredRelationsSchema, relationTypes, registerIntegrationSchema, sourceSchema, upsertInferredRelationSchema, updateIntegrationSchema, updateNodeSchema, updateRelationSchema, updateSettingsSchema, workspaceSearchSchema } from "../shared/contracts.js";
|
|
7
|
+
import { activitySearchSchema, appendActivitySchema, appendRelationUsageEventSchema, appendSearchFeedbackSchema, attachArtifactSchema, buildContextBundleSchema, captureMemorySchema, createWorkspaceBackupSchema, createWorkspaceSchema, createNodeSchema, createNodesSchema, createRelationSchema, exportWorkspaceSchema, governanceEventsQuerySchema, governanceNodeActionSchema, governanceRelationActionSchema, governanceIssuesQuerySchema, importWorkspacePreviewSchema, importWorkspaceSchema, nodeSearchSchema, openWorkspaceSchema, reindexInferredRelationsSchema, recomputeGovernanceSchema, recomputeInferredRelationsSchema, relationTypes, registerIntegrationSchema, restoreWorkspaceBackupSchema, sourceSchema, upsertInferredRelationSchema, updateIntegrationSchema, updateNodeSchema, updateRelationSchema, updateSettingsSchema, workspaceSearchSchema } from "../shared/contracts.js";
|
|
8
8
|
import { AppError } from "./errors.js";
|
|
9
9
|
import { isShortLogLikeAgentNodeInput, maybeCreatePromotionCandidate, recomputeAutomaticGovernance, resolveGovernancePolicy, resolveNodeGovernance, resolveRelationStatus, shouldPromoteActivitySummary } from "./governance.js";
|
|
10
10
|
import { refreshAutomaticInferredRelationsForNode, reindexAutomaticInferredRelations } from "./inferred-relations.js";
|
|
11
11
|
import { appendCurrentTelemetryDetails, createObservabilityWriter, summarizePayloadShape } from "./observability.js";
|
|
12
12
|
import { buildSemanticCandidateBonusMap, buildCandidateRelationBonusMap, buildContextBundle, buildNeighborhoodItems, buildNeighborhoodItemsBatch, buildTargetRelatedRetrievalItems, bundleAsMarkdown, computeRankCandidateScore, selectSemanticCandidateIds, shouldUseSemanticCandidateAugmentation } from "./retrieval.js";
|
|
13
13
|
import { buildProjectGraph } from "./project-graph.js";
|
|
14
|
+
import { isReadonlySqliteWriteError } from "./sqlite-errors.js";
|
|
14
15
|
import { createId, isPathWithinRoot } from "./utils.js";
|
|
15
16
|
const relationTypeSet = new Set(relationTypes);
|
|
16
17
|
const allowedLoopbackHostnames = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
@@ -494,6 +495,31 @@ function buildServiceIndex(workspaceInfo) {
|
|
|
494
495
|
requestExample: {
|
|
495
496
|
rootPath: "/Users/name/Documents/RecallX-Personal"
|
|
496
497
|
}
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
method: "POST",
|
|
501
|
+
path: "/api/v1/workspaces/backups",
|
|
502
|
+
purpose: "Create a manual workspace snapshot before risky changes.",
|
|
503
|
+
requestExample: {
|
|
504
|
+
label: "before-upgrade"
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
method: "POST",
|
|
509
|
+
path: "/api/v1/workspaces/restore",
|
|
510
|
+
purpose: "Restore a snapshot into a new workspace root and switch to it.",
|
|
511
|
+
requestExample: {
|
|
512
|
+
backupId: "20260330123000-before-upgrade",
|
|
513
|
+
targetRootPath: "/Users/name/Documents/RecallX-Restore"
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
method: "POST",
|
|
518
|
+
path: "/api/v1/workspaces/export",
|
|
519
|
+
purpose: "Export the active workspace to a portable file under exports/.",
|
|
520
|
+
requestExample: {
|
|
521
|
+
format: "json"
|
|
522
|
+
}
|
|
497
523
|
}
|
|
498
524
|
],
|
|
499
525
|
references: {
|
|
@@ -1008,7 +1034,9 @@ export function createRecallXApp(params) {
|
|
|
1008
1034
|
}
|
|
1009
1035
|
}
|
|
1010
1036
|
catch (error) {
|
|
1011
|
-
|
|
1037
|
+
if (!isReadonlySqliteWriteError(error)) {
|
|
1038
|
+
console.error(`Failed to refresh inferred relations for node ${nodeId}`, error);
|
|
1039
|
+
}
|
|
1012
1040
|
}
|
|
1013
1041
|
}
|
|
1014
1042
|
if (processedNodes > 0) {
|
|
@@ -1180,7 +1208,13 @@ export function createRecallXApp(params) {
|
|
|
1180
1208
|
};
|
|
1181
1209
|
}
|
|
1182
1210
|
function createDurableNodeResponse(repository, input) {
|
|
1183
|
-
const
|
|
1211
|
+
const governancePolicy = resolveGovernancePolicy(repository.getSettings(["review.autoApproveLowRisk", "review.trustedSourceToolNames"]));
|
|
1212
|
+
const rawProjectId = typeof input.metadata.projectId === "string" ? input.metadata.projectId.trim() : "";
|
|
1213
|
+
const linkedProject = rawProjectId ? repository.getNode(rawProjectId) : null;
|
|
1214
|
+
if (linkedProject && (linkedProject.type !== "project" || linkedProject.status === "archived")) {
|
|
1215
|
+
throw new AppError(400, "INVALID_INPUT", "metadata.projectId must reference an active project node.");
|
|
1216
|
+
}
|
|
1217
|
+
const governance = resolveNodeGovernance(input, governancePolicy);
|
|
1184
1218
|
const node = repository.createNode({
|
|
1185
1219
|
...input,
|
|
1186
1220
|
resolvedCanonicality: governance.canonicality,
|
|
@@ -1196,7 +1230,48 @@ export function createRecallXApp(params) {
|
|
|
1196
1230
|
}
|
|
1197
1231
|
});
|
|
1198
1232
|
const governanceResult = recomputeGovernanceForEntities("node", [node.id]);
|
|
1199
|
-
|
|
1233
|
+
if (linkedProject) {
|
|
1234
|
+
if (linkedProject.id !== node.id) {
|
|
1235
|
+
const relationInput = {
|
|
1236
|
+
fromNodeId: node.id,
|
|
1237
|
+
toNodeId: linkedProject.id,
|
|
1238
|
+
relationType: "relevant_to",
|
|
1239
|
+
source: input.source,
|
|
1240
|
+
metadata: {
|
|
1241
|
+
createdFrom: "project-aware-capture",
|
|
1242
|
+
projectId: linkedProject.id,
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
const relationGovernance = resolveRelationStatus(relationInput, governancePolicy);
|
|
1246
|
+
const relation = repository.createRelation({
|
|
1247
|
+
...relationInput,
|
|
1248
|
+
resolvedStatus: relationGovernance.status,
|
|
1249
|
+
});
|
|
1250
|
+
repository.recordProvenance({
|
|
1251
|
+
entityType: "relation",
|
|
1252
|
+
entityId: relation.id,
|
|
1253
|
+
operationType: "create",
|
|
1254
|
+
source: input.source,
|
|
1255
|
+
metadata: {
|
|
1256
|
+
reason: relationGovernance.reason,
|
|
1257
|
+
createdFrom: "project-aware-capture",
|
|
1258
|
+
},
|
|
1259
|
+
});
|
|
1260
|
+
recomputeGovernanceForEntities("relation", [relation.id]);
|
|
1261
|
+
queueInferredRefreshForNodes([node.id, linkedProject.id], "node-write");
|
|
1262
|
+
broadcastWorkspaceEvent({
|
|
1263
|
+
reason: "relation.created",
|
|
1264
|
+
entityType: "relation",
|
|
1265
|
+
entityId: relation.id,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
queueInferredRefresh(node.id, "node-write");
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
queueInferredRefresh(node.id, "node-write");
|
|
1274
|
+
}
|
|
1200
1275
|
scheduleAutoSemanticIndex();
|
|
1201
1276
|
broadcastWorkspaceEvent({
|
|
1202
1277
|
reason: "node.created",
|
|
@@ -1403,6 +1478,50 @@ export function createRecallXApp(params) {
|
|
|
1403
1478
|
const workspace = params.workspaceSessionManager.openWorkspace(input.rootPath);
|
|
1404
1479
|
commitWorkspaceMutation(response, workspace, "workspace.opened");
|
|
1405
1480
|
});
|
|
1481
|
+
app.get("/api/v1/workspaces/backups", (_request, response) => {
|
|
1482
|
+
response.json(envelope(response.locals.requestId, {
|
|
1483
|
+
items: params.workspaceSessionManager.listBackups()
|
|
1484
|
+
}));
|
|
1485
|
+
});
|
|
1486
|
+
app.post("/api/v1/workspaces/backups", (request, response) => {
|
|
1487
|
+
const input = createWorkspaceBackupSchema.parse(request.body ?? {});
|
|
1488
|
+
const backup = params.workspaceSessionManager.createBackup(input.label);
|
|
1489
|
+
response.status(201).json(envelope(response.locals.requestId, { backup }));
|
|
1490
|
+
});
|
|
1491
|
+
app.post("/api/v1/workspaces/export", (request, response) => {
|
|
1492
|
+
const input = exportWorkspaceSchema.parse(request.body ?? {});
|
|
1493
|
+
const exportRecord = params.workspaceSessionManager.exportWorkspace(input.format);
|
|
1494
|
+
response.status(201).json(envelope(response.locals.requestId, { export: exportRecord }));
|
|
1495
|
+
});
|
|
1496
|
+
app.post("/api/v1/workspaces/import/preview", (request, response) => {
|
|
1497
|
+
const input = importWorkspacePreviewSchema.parse(request.body ?? {});
|
|
1498
|
+
const preview = params.workspaceSessionManager.previewImportWorkspace(input.format, input.sourcePath, input.label, input.options);
|
|
1499
|
+
response.json(envelope(response.locals.requestId, { preview }));
|
|
1500
|
+
});
|
|
1501
|
+
app.post("/api/v1/workspaces/import", (request, response) => {
|
|
1502
|
+
const input = importWorkspaceSchema.parse(request.body ?? {});
|
|
1503
|
+
const importRecord = params.workspaceSessionManager.importWorkspace(input.format, input.sourcePath, input.label, input.options);
|
|
1504
|
+
refreshWorkspaceState();
|
|
1505
|
+
broadcastWorkspaceEvent({
|
|
1506
|
+
reason: "workspace.imported",
|
|
1507
|
+
entityType: "workspace"
|
|
1508
|
+
});
|
|
1509
|
+
response.status(201).json(envelope(response.locals.requestId, { import: importRecord }));
|
|
1510
|
+
});
|
|
1511
|
+
app.post("/api/v1/workspaces/restore", (request, response) => {
|
|
1512
|
+
const input = restoreWorkspaceBackupSchema.parse(request.body ?? {});
|
|
1513
|
+
const result = params.workspaceSessionManager.restoreBackup(input.backupId, input.targetRootPath, input.workspaceName);
|
|
1514
|
+
refreshWorkspaceState();
|
|
1515
|
+
broadcastWorkspaceEvent({
|
|
1516
|
+
reason: "workspace.restored",
|
|
1517
|
+
entityType: "workspace"
|
|
1518
|
+
});
|
|
1519
|
+
response.status(201).json(envelope(response.locals.requestId, {
|
|
1520
|
+
restoredBackupId: input.backupId,
|
|
1521
|
+
autoBackup: result.autoBackup,
|
|
1522
|
+
...buildWorkspaceMutationPayload(result.workspace)
|
|
1523
|
+
}));
|
|
1524
|
+
});
|
|
1406
1525
|
app.get("/api/v1/observability/summary", handleAsyncRoute(async (request, response) => {
|
|
1407
1526
|
const summary = await observability.summarize({
|
|
1408
1527
|
since: readRequestParam(request.query.since),
|
|
@@ -1476,6 +1595,21 @@ export function createRecallXApp(params) {
|
|
|
1476
1595
|
scopes: input.scopes
|
|
1477
1596
|
}, async (span) => {
|
|
1478
1597
|
const searchResult = await currentRepository().searchWorkspace(input, {
|
|
1598
|
+
runSearchStageSpan: async (operation, details, callback) => runObservedSpan(operation, details, async (stageSpan) => {
|
|
1599
|
+
const stageResult = await callback();
|
|
1600
|
+
if (stageResult &&
|
|
1601
|
+
typeof stageResult === "object" &&
|
|
1602
|
+
"items" in stageResult &&
|
|
1603
|
+
Array.isArray(stageResult.items) &&
|
|
1604
|
+
"total" in stageResult &&
|
|
1605
|
+
typeof stageResult.total === "number") {
|
|
1606
|
+
stageSpan.addDetails({
|
|
1607
|
+
resultCount: stageResult.items.length,
|
|
1608
|
+
totalCount: stageResult.total
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
return stageResult;
|
|
1612
|
+
}),
|
|
1479
1613
|
runSemanticFallbackSpan: async (details, callback) => runObservedSpan("workspace.search.semantic_fallback", {
|
|
1480
1614
|
...details,
|
|
1481
1615
|
mcpTool: request.header("x-recallx-mcp-tool") ?? null
|
|
@@ -1723,6 +1857,157 @@ export function createRecallXApp(params) {
|
|
|
1723
1857
|
governance: buildGovernancePayload(repository, "node", node.id, governanceResult.items[0] ?? repository.getGovernanceStateNullable("node", node.id))
|
|
1724
1858
|
}));
|
|
1725
1859
|
});
|
|
1860
|
+
app.post("/api/v1/nodes/:id/governance-action", (request, response) => {
|
|
1861
|
+
const repository = currentRepository();
|
|
1862
|
+
const input = governanceNodeActionSchema.parse(request.body ?? {});
|
|
1863
|
+
const note = input.note?.trim() ? input.note.trim() : null;
|
|
1864
|
+
const nodeId = request.params.id;
|
|
1865
|
+
const beforeNode = repository.getNode(nodeId);
|
|
1866
|
+
const previousState = repository.getGovernanceStateNullable("node", nodeId);
|
|
1867
|
+
const now = new Date().toISOString();
|
|
1868
|
+
let node = beforeNode;
|
|
1869
|
+
let nextState = previousState?.state ?? "low_confidence";
|
|
1870
|
+
let confidence = previousState?.confidence ?? 0.5;
|
|
1871
|
+
let eventType;
|
|
1872
|
+
let operationType;
|
|
1873
|
+
let reason;
|
|
1874
|
+
switch (input.action) {
|
|
1875
|
+
case "promote": {
|
|
1876
|
+
if (beforeNode.status === "archived") {
|
|
1877
|
+
throw new AppError(409, "INVALID_STATE", "Archived nodes cannot be promoted.");
|
|
1878
|
+
}
|
|
1879
|
+
node = repository.updateNode(nodeId, {
|
|
1880
|
+
status: "active",
|
|
1881
|
+
metadata: {
|
|
1882
|
+
manualGovernanceAction: "promote",
|
|
1883
|
+
manualGovernanceAt: now
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
node = repository.setNodeCanonicality(nodeId, "canonical");
|
|
1887
|
+
nextState = "healthy";
|
|
1888
|
+
confidence = Math.max(previousState?.confidence ?? 0, 0.96);
|
|
1889
|
+
eventType = "promoted";
|
|
1890
|
+
operationType = "promote";
|
|
1891
|
+
reason = note
|
|
1892
|
+
? `Human promoted this node to canonical. ${note}`
|
|
1893
|
+
: "Human promoted this node to canonical from the governance surface.";
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
case "contest": {
|
|
1897
|
+
if (beforeNode.status === "archived") {
|
|
1898
|
+
throw new AppError(409, "INVALID_STATE", "Archived nodes cannot be contested.");
|
|
1899
|
+
}
|
|
1900
|
+
node = repository.updateNode(nodeId, {
|
|
1901
|
+
status: "contested",
|
|
1902
|
+
metadata: {
|
|
1903
|
+
manualGovernanceAction: "contest",
|
|
1904
|
+
manualGovernanceAt: now
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
nextState = "contested";
|
|
1908
|
+
confidence = Math.min(previousState?.confidence ?? 0.4, 0.32);
|
|
1909
|
+
eventType = "contested";
|
|
1910
|
+
operationType = "contest";
|
|
1911
|
+
reason = note
|
|
1912
|
+
? `Human marked this node contested. ${note}`
|
|
1913
|
+
: "Human marked this node contested from the governance surface.";
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
case "archive": {
|
|
1917
|
+
if (beforeNode.status === "archived") {
|
|
1918
|
+
throw new AppError(409, "INVALID_STATE", "Node is already archived.");
|
|
1919
|
+
}
|
|
1920
|
+
node = repository.archiveNode(nodeId);
|
|
1921
|
+
nextState = previousState?.state === "contested" ? "contested" : "low_confidence";
|
|
1922
|
+
confidence = Math.min(previousState?.confidence ?? 0.4, 0.2);
|
|
1923
|
+
eventType = "demoted";
|
|
1924
|
+
operationType = "archive";
|
|
1925
|
+
reason = note
|
|
1926
|
+
? `Human archived this node from governance. ${note}`
|
|
1927
|
+
: "Human archived this node from the governance surface.";
|
|
1928
|
+
break;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
repository.recordProvenance({
|
|
1932
|
+
entityType: "node",
|
|
1933
|
+
entityId: node.id,
|
|
1934
|
+
operationType,
|
|
1935
|
+
source: input.source,
|
|
1936
|
+
metadata: {
|
|
1937
|
+
action: input.action,
|
|
1938
|
+
note,
|
|
1939
|
+
previousStatus: beforeNode.status,
|
|
1940
|
+
nextStatus: node.status,
|
|
1941
|
+
previousCanonicality: beforeNode.canonicality,
|
|
1942
|
+
nextCanonicality: node.canonicality,
|
|
1943
|
+
...input.metadata
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
const activity = repository.appendActivity({
|
|
1947
|
+
targetNodeId: node.id,
|
|
1948
|
+
activityType: "review_action",
|
|
1949
|
+
body: reason,
|
|
1950
|
+
source: input.source,
|
|
1951
|
+
metadata: {
|
|
1952
|
+
action: input.action,
|
|
1953
|
+
previousState: previousState?.state ?? "none",
|
|
1954
|
+
nextState,
|
|
1955
|
+
previousStatus: beforeNode.status,
|
|
1956
|
+
nextStatus: node.status,
|
|
1957
|
+
previousCanonicality: beforeNode.canonicality,
|
|
1958
|
+
nextCanonicality: node.canonicality,
|
|
1959
|
+
note: note ?? "",
|
|
1960
|
+
...input.metadata
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
const governanceState = repository.upsertGovernanceState({
|
|
1964
|
+
entityType: "node",
|
|
1965
|
+
entityId: node.id,
|
|
1966
|
+
state: nextState,
|
|
1967
|
+
confidence,
|
|
1968
|
+
reasons: [reason],
|
|
1969
|
+
lastEvaluatedAt: now,
|
|
1970
|
+
metadata: {
|
|
1971
|
+
manualAction: input.action,
|
|
1972
|
+
note: note ?? "",
|
|
1973
|
+
source: input.source.actorLabel
|
|
1974
|
+
},
|
|
1975
|
+
previousState
|
|
1976
|
+
});
|
|
1977
|
+
repository.appendGovernanceEvent({
|
|
1978
|
+
entityType: "node",
|
|
1979
|
+
entityId: node.id,
|
|
1980
|
+
eventType,
|
|
1981
|
+
previousState: previousState?.state ?? null,
|
|
1982
|
+
nextState,
|
|
1983
|
+
confidence,
|
|
1984
|
+
reason,
|
|
1985
|
+
metadata: {
|
|
1986
|
+
manualAction: input.action,
|
|
1987
|
+
note: note ?? "",
|
|
1988
|
+
previousStatus: beforeNode.status,
|
|
1989
|
+
nextStatus: node.status,
|
|
1990
|
+
previousCanonicality: beforeNode.canonicality,
|
|
1991
|
+
nextCanonicality: node.canonicality
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
queueInferredRefresh(node.id, "node-write");
|
|
1995
|
+
scheduleAutoSemanticIndex();
|
|
1996
|
+
broadcastWorkspaceEvent({
|
|
1997
|
+
reason: input.action === "promote"
|
|
1998
|
+
? "node.promoted"
|
|
1999
|
+
: input.action === "contest"
|
|
2000
|
+
? "node.contested"
|
|
2001
|
+
: "node.archived",
|
|
2002
|
+
entityType: "node",
|
|
2003
|
+
entityId: node.id
|
|
2004
|
+
});
|
|
2005
|
+
response.json(envelope(response.locals.requestId, {
|
|
2006
|
+
node,
|
|
2007
|
+
activity,
|
|
2008
|
+
governance: buildGovernancePayload(repository, "node", node.id, governanceState)
|
|
2009
|
+
}));
|
|
2010
|
+
});
|
|
1726
2011
|
app.post("/api/v1/nodes/:id/archive", (request, response) => {
|
|
1727
2012
|
const repository = currentRepository();
|
|
1728
2013
|
const source = sourceSchema.parse(request.body?.source ?? request.body ?? {});
|
|
@@ -2014,6 +2299,117 @@ export function createRecallXApp(params) {
|
|
|
2014
2299
|
governance: buildGovernancePayload(repository, "relation", relation.id, governanceResult.items[0] ?? repository.getGovernanceStateNullable("relation", relation.id))
|
|
2015
2300
|
}));
|
|
2016
2301
|
});
|
|
2302
|
+
app.get("/api/v1/relations/:id", (request, response) => {
|
|
2303
|
+
const repository = currentRepository();
|
|
2304
|
+
const relation = repository.getRelation(request.params.id);
|
|
2305
|
+
response.json(envelope(response.locals.requestId, {
|
|
2306
|
+
relation,
|
|
2307
|
+
fromNode: repository.getNode(relation.fromNodeId),
|
|
2308
|
+
toNode: repository.getNode(relation.toNodeId),
|
|
2309
|
+
governance: buildGovernancePayload(repository, "relation", relation.id)
|
|
2310
|
+
}));
|
|
2311
|
+
});
|
|
2312
|
+
app.post("/api/v1/relations/:id/governance-action", (request, response) => {
|
|
2313
|
+
const repository = currentRepository();
|
|
2314
|
+
const input = governanceRelationActionSchema.parse(request.body ?? {});
|
|
2315
|
+
const note = input.note?.trim() ? input.note.trim() : null;
|
|
2316
|
+
const relationId = request.params.id;
|
|
2317
|
+
const beforeRelation = repository.getRelation(relationId);
|
|
2318
|
+
const previousState = repository.getGovernanceStateNullable("relation", relationId);
|
|
2319
|
+
if (beforeRelation.status === "archived") {
|
|
2320
|
+
throw new AppError(409, "INVALID_STATE", "Archived relations cannot be changed with governance actions.");
|
|
2321
|
+
}
|
|
2322
|
+
let nextStatus;
|
|
2323
|
+
let confidence = 0.92;
|
|
2324
|
+
let eventType;
|
|
2325
|
+
let operationType;
|
|
2326
|
+
let reason;
|
|
2327
|
+
switch (input.action) {
|
|
2328
|
+
case "accept":
|
|
2329
|
+
nextStatus = "active";
|
|
2330
|
+
confidence = Math.max(previousState?.confidence ?? 0, 0.94);
|
|
2331
|
+
eventType = "promoted";
|
|
2332
|
+
operationType = "approve";
|
|
2333
|
+
reason = note
|
|
2334
|
+
? `Human accepted this relation. ${note}`
|
|
2335
|
+
: "Human accepted this relation from the governance surface.";
|
|
2336
|
+
break;
|
|
2337
|
+
case "reject":
|
|
2338
|
+
nextStatus = "rejected";
|
|
2339
|
+
confidence = Math.max(previousState?.confidence ?? 0, 0.9);
|
|
2340
|
+
eventType = "demoted";
|
|
2341
|
+
operationType = "reject";
|
|
2342
|
+
reason = note
|
|
2343
|
+
? `Human rejected this relation. ${note}`
|
|
2344
|
+
: "Human rejected this relation from the governance surface.";
|
|
2345
|
+
break;
|
|
2346
|
+
case "archive":
|
|
2347
|
+
nextStatus = "archived";
|
|
2348
|
+
confidence = Math.max(previousState?.confidence ?? 0, 0.88);
|
|
2349
|
+
eventType = "demoted";
|
|
2350
|
+
operationType = "archive";
|
|
2351
|
+
reason = note
|
|
2352
|
+
? `Human archived this relation from governance. ${note}`
|
|
2353
|
+
: "Human archived this relation from the governance surface.";
|
|
2354
|
+
break;
|
|
2355
|
+
}
|
|
2356
|
+
const relation = repository.updateRelationStatus(relationId, nextStatus);
|
|
2357
|
+
repository.recordProvenance({
|
|
2358
|
+
entityType: "relation",
|
|
2359
|
+
entityId: relation.id,
|
|
2360
|
+
operationType,
|
|
2361
|
+
source: input.source,
|
|
2362
|
+
metadata: {
|
|
2363
|
+
action: input.action,
|
|
2364
|
+
note,
|
|
2365
|
+
previousStatus: beforeRelation.status,
|
|
2366
|
+
nextStatus: relation.status,
|
|
2367
|
+
...input.metadata
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
const governanceState = repository.upsertGovernanceState({
|
|
2371
|
+
entityType: "relation",
|
|
2372
|
+
entityId: relation.id,
|
|
2373
|
+
state: "healthy",
|
|
2374
|
+
confidence,
|
|
2375
|
+
reasons: [reason],
|
|
2376
|
+
metadata: {
|
|
2377
|
+
manualAction: input.action,
|
|
2378
|
+
note: note ?? "",
|
|
2379
|
+
source: input.source.actorLabel
|
|
2380
|
+
},
|
|
2381
|
+
previousState
|
|
2382
|
+
});
|
|
2383
|
+
repository.appendGovernanceEvent({
|
|
2384
|
+
entityType: "relation",
|
|
2385
|
+
entityId: relation.id,
|
|
2386
|
+
eventType,
|
|
2387
|
+
previousState: previousState?.state ?? null,
|
|
2388
|
+
nextState: "healthy",
|
|
2389
|
+
confidence,
|
|
2390
|
+
reason,
|
|
2391
|
+
metadata: {
|
|
2392
|
+
manualAction: input.action,
|
|
2393
|
+
note: note ?? "",
|
|
2394
|
+
previousStatus: beforeRelation.status,
|
|
2395
|
+
nextStatus: relation.status
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
queueInferredRefreshForNodes([relation.fromNodeId, relation.toNodeId], "node-write");
|
|
2399
|
+
broadcastWorkspaceEvent({
|
|
2400
|
+
reason: input.action === "accept"
|
|
2401
|
+
? "relation.accepted"
|
|
2402
|
+
: input.action === "reject"
|
|
2403
|
+
? "relation.rejected"
|
|
2404
|
+
: "relation.archived",
|
|
2405
|
+
entityType: "relation",
|
|
2406
|
+
entityId: relation.id
|
|
2407
|
+
});
|
|
2408
|
+
response.json(envelope(response.locals.requestId, {
|
|
2409
|
+
relation,
|
|
2410
|
+
governance: buildGovernancePayload(repository, "relation", relation.id, governanceState)
|
|
2411
|
+
}));
|
|
2412
|
+
});
|
|
2017
2413
|
app.get("/api/v1/nodes/:id/activities", (request, response) => {
|
|
2018
2414
|
const limit = Number(request.query.limit ?? 20);
|
|
2019
2415
|
response.json(envelope(response.locals.requestId, {
|
|
@@ -2233,6 +2629,18 @@ export function createRecallXApp(params) {
|
|
|
2233
2629
|
items: currentRepository().listGovernanceIssues(input.limit, input.states)
|
|
2234
2630
|
}));
|
|
2235
2631
|
});
|
|
2632
|
+
app.get("/api/v1/governance/events", (request, response) => {
|
|
2633
|
+
const entityTypes = parseCommaSeparatedValues(request.query.entity_types)?.filter((entityType) => entityType === "node" || entityType === "relation");
|
|
2634
|
+
const actions = parseCommaSeparatedValues(request.query.actions)?.filter((action) => action === "promote" || action === "contest" || action === "archive" || action === "accept" || action === "reject");
|
|
2635
|
+
const input = governanceEventsQuerySchema.parse({
|
|
2636
|
+
entityTypes,
|
|
2637
|
+
actions,
|
|
2638
|
+
limit: Number(request.query.limit ?? 12)
|
|
2639
|
+
});
|
|
2640
|
+
response.json(envelope(response.locals.requestId, {
|
|
2641
|
+
items: currentRepository().listRecentGovernanceEvents(input)
|
|
2642
|
+
}));
|
|
2643
|
+
});
|
|
2236
2644
|
app.get("/api/v1/governance/state/:entityType/:id", (request, response) => {
|
|
2237
2645
|
const entityType = readRequestParam(request.params.entityType);
|
|
2238
2646
|
if (entityType !== "node" && entityType !== "relation") {
|
package/app/server/config.js
CHANGED
|
@@ -15,13 +15,15 @@ export function ensureApiToken(config) {
|
|
|
15
15
|
}
|
|
16
16
|
return randomBytes(24).toString("hex");
|
|
17
17
|
}
|
|
18
|
-
export function workspaceInfo(rootPath, config, authMode) {
|
|
18
|
+
export function workspaceInfo(rootPath, config, authMode, paths, safety) {
|
|
19
19
|
return {
|
|
20
20
|
rootPath,
|
|
21
21
|
workspaceName: config.workspaceName,
|
|
22
22
|
schemaVersion: getSchemaVersion(),
|
|
23
23
|
bindAddress: `${config.bindAddress}:${config.port}`,
|
|
24
24
|
enabledIntegrationModes: ["read-only", "append-only"],
|
|
25
|
-
authMode
|
|
25
|
+
authMode,
|
|
26
|
+
paths,
|
|
27
|
+
safety,
|
|
26
28
|
};
|
|
27
29
|
}
|
package/app/server/index.js
CHANGED
|
@@ -11,7 +11,18 @@ const app = createRecallXApp({
|
|
|
11
11
|
workspaceSessionManager,
|
|
12
12
|
apiToken: config.apiToken ? apiToken : null
|
|
13
13
|
});
|
|
14
|
-
createServer(app)
|
|
14
|
+
const server = createServer(app);
|
|
15
|
+
function shutdown() {
|
|
16
|
+
try {
|
|
17
|
+
workspaceSessionManager.shutdown();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
process.once("SIGINT", shutdown);
|
|
24
|
+
process.once("SIGTERM", shutdown);
|
|
25
|
+
server.listen(config.port, config.bindAddress, () => {
|
|
15
26
|
console.log(`RecallX API listening on http://${config.bindAddress}:${config.port}`);
|
|
16
27
|
if (resolveRendererDistDir()) {
|
|
17
28
|
console.log(`RecallX UI available at http://${config.bindAddress}:${config.port}/`);
|
|
@@ -6,6 +6,9 @@ const DEFAULT_PROJECT_INFERRED_LIMIT = 60;
|
|
|
6
6
|
function relationLabel(value) {
|
|
7
7
|
return value.replaceAll("_", " ");
|
|
8
8
|
}
|
|
9
|
+
function relationEdgeKey(sourceNodeId, targetNodeId) {
|
|
10
|
+
return `${sourceNodeId}:${targetNodeId}`;
|
|
11
|
+
}
|
|
9
12
|
export function buildProjectGraph(repository, projectId, options) {
|
|
10
13
|
const project = repository.getNode(projectId);
|
|
11
14
|
if (project.type !== "project") {
|
|
@@ -37,11 +40,11 @@ export function buildProjectGraph(repository, projectId, options) {
|
|
|
37
40
|
.items
|
|
38
41
|
.filter((item) => item.id !== projectId)
|
|
39
42
|
.map((item) => item.id)
|
|
40
|
-
.filter((nodeId, index, items) => items.indexOf(nodeId) === index)
|
|
41
43
|
: [];
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const uniqueFallbackNodeIds = Array.from(new Set(fallbackNodeIds));
|
|
45
|
+
const fallbackNodeMap = uniqueFallbackNodeIds.length > 0 ? repository.getNodesByIds(uniqueFallbackNodeIds) : new Map();
|
|
46
|
+
const fallbackNodes = uniqueFallbackNodeIds.length > 0
|
|
47
|
+
? uniqueFallbackNodeIds
|
|
45
48
|
.map((nodeId) => fallbackNodeMap.get(nodeId))
|
|
46
49
|
.filter((node) => Boolean(node))
|
|
47
50
|
: [];
|
|
@@ -54,9 +57,13 @@ export function buildProjectGraph(repository, projectId, options) {
|
|
|
54
57
|
canonicalEdges = repository.listRelationsBetweenNodeIds(expandedScopedNodeIds);
|
|
55
58
|
inferredEdges = includeInferred ? repository.listInferredRelationsBetweenNodeIds(expandedScopedNodeIds, inferredLimit) : [];
|
|
56
59
|
}
|
|
57
|
-
const
|
|
60
|
+
const connectedEdgeKeys = new Set([
|
|
61
|
+
...canonicalEdges.map((edge) => relationEdgeKey(edge.fromNodeId, edge.toNodeId)),
|
|
62
|
+
...inferredEdges.map((edge) => relationEdgeKey(edge.fromNodeId, edge.toNodeId)),
|
|
63
|
+
]);
|
|
58
64
|
const syntheticFallbackEdges = fallbackNodes
|
|
59
|
-
.filter((node) => !
|
|
65
|
+
.filter((node) => !connectedEdgeKeys.has(relationEdgeKey(node.id, projectId)) &&
|
|
66
|
+
!connectedEdgeKeys.has(relationEdgeKey(projectId, node.id)))
|
|
60
67
|
.map((node) => ({
|
|
61
68
|
id: `project-map-fallback:${projectId}:${node.id}`,
|
|
62
69
|
source: projectId,
|