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
|
@@ -1290,6 +1290,12 @@ export class RecallXRepository {
|
|
|
1290
1290
|
tags: parseJson(row.tags_json, [])
|
|
1291
1291
|
}));
|
|
1292
1292
|
}
|
|
1293
|
+
listAllNodes() {
|
|
1294
|
+
const rows = this.db
|
|
1295
|
+
.prepare(`SELECT * FROM nodes ORDER BY updated_at DESC, id DESC`)
|
|
1296
|
+
.all();
|
|
1297
|
+
return rows.map(mapNode);
|
|
1298
|
+
}
|
|
1293
1299
|
listActiveNodesByType(type, limit = 20) {
|
|
1294
1300
|
const rows = this.db
|
|
1295
1301
|
.prepare(`SELECT *
|
|
@@ -1594,37 +1600,60 @@ export class RecallXRepository {
|
|
|
1594
1600
|
const queryPresent = Boolean(input.query.trim());
|
|
1595
1601
|
const searchSort = input.sort === "smart" ? (queryPresent ? "relevance" : "updated_at") : input.sort;
|
|
1596
1602
|
const normalizedQuery = input.query.trim();
|
|
1603
|
+
const runSearchStage = async (operation, details, callback) => {
|
|
1604
|
+
if (!options.runSearchStageSpan) {
|
|
1605
|
+
return await callback();
|
|
1606
|
+
}
|
|
1607
|
+
return await options.runSearchStageSpan(operation, details, callback);
|
|
1608
|
+
};
|
|
1597
1609
|
const nodeResults = includeNodes
|
|
1598
|
-
?
|
|
1610
|
+
? await runSearchStage("workspace.search.nodes.deterministic", {
|
|
1611
|
+
queryPresent,
|
|
1612
|
+
limit: requestedWindow,
|
|
1613
|
+
sort: searchSort
|
|
1614
|
+
}, () => this.searchNodes({
|
|
1599
1615
|
query: input.query,
|
|
1600
1616
|
filters: input.nodeFilters ?? {},
|
|
1601
1617
|
limit: requestedWindow,
|
|
1602
1618
|
offset: 0,
|
|
1603
1619
|
sort: searchSort
|
|
1604
|
-
})
|
|
1620
|
+
}))
|
|
1605
1621
|
: { items: [], total: 0 };
|
|
1606
1622
|
const activityResults = includeActivities
|
|
1607
|
-
?
|
|
1623
|
+
? await runSearchStage("workspace.search.activities.deterministic", {
|
|
1624
|
+
queryPresent,
|
|
1625
|
+
limit: requestedWindow,
|
|
1626
|
+
sort: searchSort
|
|
1627
|
+
}, () => this.searchActivities({
|
|
1608
1628
|
query: input.query,
|
|
1609
1629
|
filters: input.activityFilters ?? {},
|
|
1610
1630
|
limit: requestedWindow,
|
|
1611
1631
|
offset: 0,
|
|
1612
1632
|
sort: searchSort
|
|
1613
|
-
})
|
|
1633
|
+
}))
|
|
1614
1634
|
: { items: [], total: 0 };
|
|
1615
1635
|
const fallbackTriggered = queryPresent && nodeResults.total + activityResults.total === 0;
|
|
1616
1636
|
const fallbackTokens = fallbackTriggered ? tokenizeSearchQuery(input.query, SEARCH_FALLBACK_TOKEN_LIMIT) : [];
|
|
1617
1637
|
const resolvedNodeResults = fallbackTokens.length >= 2 && includeNodes
|
|
1618
|
-
?
|
|
1638
|
+
? await runSearchStage("workspace.search.nodes.fallback_token", {
|
|
1639
|
+
fallbackTokenCount: fallbackTokens.length,
|
|
1640
|
+
limit: requestedWindow,
|
|
1641
|
+
queryPresent
|
|
1642
|
+
}, () => this.searchWorkspaceNodeFallback(fallbackTokens, input.nodeFilters ?? {}, requestedWindow))
|
|
1619
1643
|
: nodeResults;
|
|
1620
1644
|
const resolvedActivityResults = fallbackTokens.length >= 2 && includeActivities
|
|
1621
|
-
?
|
|
1645
|
+
? await runSearchStage("workspace.search.activities.fallback_token", {
|
|
1646
|
+
fallbackTokenCount: fallbackTokens.length,
|
|
1647
|
+
limit: requestedWindow,
|
|
1648
|
+
queryPresent
|
|
1649
|
+
}, () => this.searchWorkspaceActivityFallback(fallbackTokens, input.activityFilters ?? {}, requestedWindow))
|
|
1622
1650
|
: activityResults;
|
|
1623
1651
|
const bestNodeLexicalQuality = summarizeLexicalQuality(resolvedNodeResults.items);
|
|
1624
1652
|
const bestActivityLexicalQuality = summarizeLexicalQuality(resolvedActivityResults.items);
|
|
1625
1653
|
const merged = this.mergeWorkspaceSearchResults(resolvedNodeResults.items, resolvedActivityResults.items, input.sort);
|
|
1654
|
+
const fallbackTokenUsed = fallbackTokens.length >= 2;
|
|
1626
1655
|
const deterministicResult = {
|
|
1627
|
-
total:
|
|
1656
|
+
total: fallbackTokenUsed
|
|
1628
1657
|
? merged.length
|
|
1629
1658
|
: resolvedNodeResults.total + resolvedActivityResults.total,
|
|
1630
1659
|
items: merged.slice(input.offset, input.offset + input.limit)
|
|
@@ -1647,15 +1676,23 @@ export class RecallXRepository {
|
|
|
1647
1676
|
const activityItems = result.items.flatMap((item) => item.resultType === "activity" && item.activity ? [item.activity] : []);
|
|
1648
1677
|
appendCurrentTelemetryDetails({
|
|
1649
1678
|
searchHit: result.items.length > 0,
|
|
1679
|
+
requestedWindow,
|
|
1650
1680
|
candidateCount: requestedWindow,
|
|
1681
|
+
rawNodeTotalCount: nodeResults.total,
|
|
1682
|
+
rawActivityTotalCount: activityResults.total,
|
|
1683
|
+
resolvedNodeTotalCount: resolvedNodeResults.total,
|
|
1684
|
+
resolvedActivityTotalCount: resolvedActivityResults.total,
|
|
1651
1685
|
nodeCandidateCount: resolvedNodeResults.items.length,
|
|
1652
1686
|
activityCandidateCount: resolvedActivityResults.items.length,
|
|
1687
|
+
mergedCandidateCount: merged.length,
|
|
1653
1688
|
nodeResultCount: nodeItems.length,
|
|
1654
1689
|
activityResultCount: activityItems.length,
|
|
1655
1690
|
bestNodeLexicalQuality,
|
|
1656
1691
|
bestActivityLexicalQuality,
|
|
1657
1692
|
lexicalNodeHit: bestNodeLexicalQuality !== "none",
|
|
1658
1693
|
strongNodeLexicalHit: bestNodeLexicalQuality === "strong",
|
|
1694
|
+
queryFallbackTriggered: fallbackTriggered,
|
|
1695
|
+
queryFallbackUsed: fallbackTokenUsed,
|
|
1659
1696
|
resultComposition: computeWorkspaceResultComposition({
|
|
1660
1697
|
nodeCount: nodeItems.length,
|
|
1661
1698
|
activityCount: activityItems.length,
|
|
@@ -1663,7 +1700,8 @@ export class RecallXRepository {
|
|
|
1663
1700
|
}),
|
|
1664
1701
|
resultCount: result.items.length,
|
|
1665
1702
|
totalCount: result.total,
|
|
1666
|
-
|
|
1703
|
+
queryFallbackTermCount: fallbackTokens.length,
|
|
1704
|
+
totalCountStrategy: fallbackTokenUsed ? "merged_length" : "source_total_sum",
|
|
1667
1705
|
semanticFallbackEligible: telemetry.semanticFallbackEligible,
|
|
1668
1706
|
semanticFallbackAttempted: telemetry.semanticFallbackAttempted,
|
|
1669
1707
|
semanticFallbackUsed: telemetry.semanticFallbackUsed,
|
|
@@ -2047,6 +2085,7 @@ export class RecallXRepository {
|
|
|
2047
2085
|
a.body,
|
|
2048
2086
|
a.source_label,
|
|
2049
2087
|
a.created_at,
|
|
2088
|
+
a.metadata_json,
|
|
2050
2089
|
n.title AS target_title,
|
|
2051
2090
|
n.type AS target_type,
|
|
2052
2091
|
n.status AS target_status
|
|
@@ -2074,6 +2113,7 @@ export class RecallXRepository {
|
|
|
2074
2113
|
body: row.body ? String(row.body) : null,
|
|
2075
2114
|
sourceLabel: row.source_label ? String(row.source_label) : null,
|
|
2076
2115
|
createdAt: String(row.created_at),
|
|
2116
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
2077
2117
|
lexicalQuality,
|
|
2078
2118
|
matchReason: buildSearchMatchReason(params.strategy, signals.matchedFields, {
|
|
2079
2119
|
strength: lexicalQuality === "none" ? undefined : lexicalQuality,
|
|
@@ -2417,6 +2457,12 @@ export class RecallXRepository {
|
|
|
2417
2457
|
.all(...uniqueIds, ...uniqueIds);
|
|
2418
2458
|
return rows.map(mapRelation);
|
|
2419
2459
|
}
|
|
2460
|
+
listAllRelations() {
|
|
2461
|
+
const rows = this.db
|
|
2462
|
+
.prepare(`SELECT * FROM relations ORDER BY created_at ASC, id ASC`)
|
|
2463
|
+
.all();
|
|
2464
|
+
return rows.map(mapRelation);
|
|
2465
|
+
}
|
|
2420
2466
|
createRelation(input) {
|
|
2421
2467
|
const now = nowIso();
|
|
2422
2468
|
const id = createId("rel");
|
|
@@ -2720,6 +2766,60 @@ export class RecallXRepository {
|
|
|
2720
2766
|
.all(entityType, entityId, limit);
|
|
2721
2767
|
return rows.map(mapGovernanceEvent);
|
|
2722
2768
|
}
|
|
2769
|
+
listRecentGovernanceEvents(options) {
|
|
2770
|
+
const limit = options?.limit ?? 12;
|
|
2771
|
+
const entityTypes = options?.entityTypes?.length ? options.entityTypes : undefined;
|
|
2772
|
+
const actions = options?.actions?.length ? options.actions : undefined;
|
|
2773
|
+
const where = [`json_extract(ge.metadata_json, '$.manualAction') IS NOT NULL`];
|
|
2774
|
+
const params = [];
|
|
2775
|
+
if (entityTypes?.length) {
|
|
2776
|
+
where.push(`ge.entity_type IN (${entityTypes.map(() => "?").join(", ")})`);
|
|
2777
|
+
params.push(...entityTypes);
|
|
2778
|
+
}
|
|
2779
|
+
if (actions?.length) {
|
|
2780
|
+
where.push(`json_extract(ge.metadata_json, '$.manualAction') IN (${actions.map(() => "?").join(", ")})`);
|
|
2781
|
+
params.push(...actions);
|
|
2782
|
+
}
|
|
2783
|
+
const rows = this.db
|
|
2784
|
+
.prepare(`SELECT
|
|
2785
|
+
ge.*,
|
|
2786
|
+
json_extract(ge.metadata_json, '$.manualAction') AS manual_action,
|
|
2787
|
+
CASE
|
|
2788
|
+
WHEN ge.entity_type = 'node' THEN n.title
|
|
2789
|
+
ELSE COALESCE(fn.title, r.from_node_id) || ' ' || r.relation_type || ' ' || COALESCE(tn.title, r.to_node_id)
|
|
2790
|
+
END AS display_title,
|
|
2791
|
+
CASE
|
|
2792
|
+
WHEN ge.entity_type = 'node' THEN n.type
|
|
2793
|
+
ELSE r.status
|
|
2794
|
+
END AS display_subtitle,
|
|
2795
|
+
CASE WHEN ge.entity_type = 'node' THEN ge.entity_id ELSE NULL END AS node_id,
|
|
2796
|
+
CASE WHEN ge.entity_type = 'relation' THEN r.from_node_id ELSE NULL END AS from_node_id,
|
|
2797
|
+
CASE WHEN ge.entity_type = 'relation' THEN r.to_node_id ELSE NULL END AS to_node_id,
|
|
2798
|
+
CASE WHEN ge.entity_type = 'relation' THEN r.relation_type ELSE NULL END AS relation_type
|
|
2799
|
+
FROM governance_events ge
|
|
2800
|
+
LEFT JOIN nodes n
|
|
2801
|
+
ON ge.entity_type = 'node'
|
|
2802
|
+
AND ge.entity_id = n.id
|
|
2803
|
+
LEFT JOIN relations r
|
|
2804
|
+
ON ge.entity_type = 'relation'
|
|
2805
|
+
AND ge.entity_id = r.id
|
|
2806
|
+
LEFT JOIN nodes fn ON fn.id = r.from_node_id
|
|
2807
|
+
LEFT JOIN nodes tn ON tn.id = r.to_node_id
|
|
2808
|
+
WHERE ${where.join(" AND ")}
|
|
2809
|
+
ORDER BY ge.created_at DESC
|
|
2810
|
+
LIMIT ?`)
|
|
2811
|
+
.all(...params, limit);
|
|
2812
|
+
return rows.map((row) => ({
|
|
2813
|
+
...mapGovernanceEvent(row),
|
|
2814
|
+
action: row.manual_action ? String(row.manual_action) : null,
|
|
2815
|
+
title: row.display_title ? String(row.display_title) : null,
|
|
2816
|
+
subtitle: row.display_subtitle ? String(row.display_subtitle) : null,
|
|
2817
|
+
nodeId: row.node_id ? String(row.node_id) : null,
|
|
2818
|
+
fromNodeId: row.from_node_id ? String(row.from_node_id) : null,
|
|
2819
|
+
toNodeId: row.to_node_id ? String(row.to_node_id) : null,
|
|
2820
|
+
relationType: row.relation_type ? String(row.relation_type) : null
|
|
2821
|
+
}));
|
|
2822
|
+
}
|
|
2723
2823
|
upsertGovernanceState(params) {
|
|
2724
2824
|
const now = params.lastEvaluatedAt ?? nowIso();
|
|
2725
2825
|
const existing = params.previousState === undefined
|
|
@@ -3028,6 +3128,12 @@ export class RecallXRepository {
|
|
|
3028
3128
|
.all(...uniqueIds, limit);
|
|
3029
3129
|
return rows.map(mapActivity);
|
|
3030
3130
|
}
|
|
3131
|
+
listAllActivities() {
|
|
3132
|
+
const rows = this.db
|
|
3133
|
+
.prepare(`SELECT * FROM activities ORDER BY created_at ASC, id ASC`)
|
|
3134
|
+
.all();
|
|
3135
|
+
return rows.map(mapActivity);
|
|
3136
|
+
}
|
|
3031
3137
|
appendActivity(input) {
|
|
3032
3138
|
const id = createId("act");
|
|
3033
3139
|
const now = nowIso();
|
|
@@ -3110,6 +3216,12 @@ export class RecallXRepository {
|
|
|
3110
3216
|
.all(nodeId);
|
|
3111
3217
|
return rows.map(mapArtifact);
|
|
3112
3218
|
}
|
|
3219
|
+
listAllArtifacts() {
|
|
3220
|
+
const rows = this.db
|
|
3221
|
+
.prepare(`SELECT * FROM artifacts ORDER BY created_at DESC, id DESC`)
|
|
3222
|
+
.all();
|
|
3223
|
+
return rows.map(mapArtifact);
|
|
3224
|
+
}
|
|
3113
3225
|
getArtifact(id) {
|
|
3114
3226
|
const row = this.db.prepare(`SELECT * FROM artifacts WHERE id = ?`).get(id);
|
|
3115
3227
|
return mapArtifact(assertPresent(row, `Artifact ${id} not found`));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function isReadonlySqliteWriteError(error) {
|
|
2
|
+
if (!error || typeof error !== "object") {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const candidate = error;
|
|
6
|
+
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
7
|
+
const errstr = typeof candidate.errstr === "string" ? candidate.errstr.toLowerCase() : "";
|
|
8
|
+
const message = typeof candidate.message === "string" ? candidate.message.toLowerCase() : "";
|
|
9
|
+
return code === "ERR_SQLITE_ERROR" && (errstr.includes("readonly database") || message.includes("readonly database"));
|
|
10
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export const DEFAULT_IMPORT_OPTIONS = {
|
|
2
|
+
normalizeTitleWhitespace: true,
|
|
3
|
+
trimBodyWhitespace: false,
|
|
4
|
+
duplicateMode: "warn",
|
|
5
|
+
};
|
|
6
|
+
export function resolveImportOptions(options) {
|
|
7
|
+
return {
|
|
8
|
+
normalizeTitleWhitespace: typeof options?.normalizeTitleWhitespace === "boolean"
|
|
9
|
+
? options.normalizeTitleWhitespace
|
|
10
|
+
: DEFAULT_IMPORT_OPTIONS.normalizeTitleWhitespace,
|
|
11
|
+
trimBodyWhitespace: typeof options?.trimBodyWhitespace === "boolean"
|
|
12
|
+
? options.trimBodyWhitespace
|
|
13
|
+
: DEFAULT_IMPORT_OPTIONS.trimBodyWhitespace,
|
|
14
|
+
duplicateMode: options?.duplicateMode === "skip_exact" || options?.duplicateMode === "warn"
|
|
15
|
+
? options.duplicateMode
|
|
16
|
+
: DEFAULT_IMPORT_OPTIONS.duplicateMode,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function normalizeTitle(value, options) {
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (!options.normalizeTitleWhitespace) {
|
|
22
|
+
return trimmed || "Imported node";
|
|
23
|
+
}
|
|
24
|
+
return trimmed.replace(/\s+/g, " ").trim() || "Imported node";
|
|
25
|
+
}
|
|
26
|
+
export function normalizeBody(value, options) {
|
|
27
|
+
const unix = value.replace(/\r\n/g, "\n");
|
|
28
|
+
if (!options.trimBodyWhitespace) {
|
|
29
|
+
return unix;
|
|
30
|
+
}
|
|
31
|
+
return unix.replace(/[ \t]+$/gm, "").replace(/\s+$/u, "");
|
|
32
|
+
}
|
|
33
|
+
export function buildTitleKey(title, options) {
|
|
34
|
+
return normalizeTitle(title, options).toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
export function buildExactKey(type, title, body, options) {
|
|
37
|
+
return `${type}::${buildTitleKey(title, options)}::${normalizeBody(body, options)}`;
|
|
38
|
+
}
|
|
39
|
+
export function buildDuplicateIndex(existingNodes, options) {
|
|
40
|
+
const exact = new Map();
|
|
41
|
+
const title = new Map();
|
|
42
|
+
for (const node of existingNodes) {
|
|
43
|
+
const nodeType = node.type;
|
|
44
|
+
const nodeTitle = node.title ?? node.id;
|
|
45
|
+
const nodeBody = node.body ?? "";
|
|
46
|
+
const exactKey = buildExactKey(nodeType, nodeTitle, nodeBody, options);
|
|
47
|
+
const titleKey = buildTitleKey(nodeTitle, options);
|
|
48
|
+
exact.set(exactKey, [...(exact.get(exactKey) ?? []), node]);
|
|
49
|
+
title.set(titleKey, [...(title.get(titleKey) ?? []), node]);
|
|
50
|
+
}
|
|
51
|
+
return { exact, title };
|
|
52
|
+
}
|
|
53
|
+
export function detectDuplicateMatch(params) {
|
|
54
|
+
const exactKey = buildExactKey(params.node.type, params.node.title, params.node.body, params.options);
|
|
55
|
+
const titleKey = buildTitleKey(params.node.title, params.options);
|
|
56
|
+
const workspaceExact = params.existing.exact.get(exactKey)?.[0];
|
|
57
|
+
if (workspaceExact) {
|
|
58
|
+
return {
|
|
59
|
+
title: params.node.title,
|
|
60
|
+
sourcePath: params.node.sourcePath,
|
|
61
|
+
matchType: "exact",
|
|
62
|
+
existingNodeId: workspaceExact.id,
|
|
63
|
+
existingNodeTitle: workspaceExact.title,
|
|
64
|
+
existingSource: "workspace",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const batchExact = params.seen.exact.get(exactKey);
|
|
68
|
+
if (batchExact) {
|
|
69
|
+
return {
|
|
70
|
+
title: params.node.title,
|
|
71
|
+
sourcePath: params.node.sourcePath,
|
|
72
|
+
matchType: "exact",
|
|
73
|
+
existingNodeId: null,
|
|
74
|
+
existingNodeTitle: batchExact.title,
|
|
75
|
+
existingSource: "batch",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const workspaceTitle = params.existing.title.get(titleKey)?.[0];
|
|
79
|
+
if (workspaceTitle) {
|
|
80
|
+
return {
|
|
81
|
+
title: params.node.title,
|
|
82
|
+
sourcePath: params.node.sourcePath,
|
|
83
|
+
matchType: "title",
|
|
84
|
+
existingNodeId: workspaceTitle.id,
|
|
85
|
+
existingNodeTitle: workspaceTitle.title,
|
|
86
|
+
existingSource: "workspace",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const batchTitle = params.seen.title.get(titleKey);
|
|
90
|
+
if (batchTitle) {
|
|
91
|
+
return {
|
|
92
|
+
title: params.node.title,
|
|
93
|
+
sourcePath: params.node.sourcePath,
|
|
94
|
+
matchType: "title",
|
|
95
|
+
existingNodeId: null,
|
|
96
|
+
existingNodeTitle: batchTitle.title,
|
|
97
|
+
existingSource: "batch",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
export function rememberSeenNode(node, options, seen) {
|
|
103
|
+
const exactKey = buildExactKey(node.type, node.title, node.body, options);
|
|
104
|
+
const titleKey = buildTitleKey(node.title, options);
|
|
105
|
+
if (!seen.exact.has(exactKey)) {
|
|
106
|
+
seen.exact.set(exactKey, node);
|
|
107
|
+
}
|
|
108
|
+
if (!seen.title.has(titleKey)) {
|
|
109
|
+
seen.title.set(titleKey, node);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function buildPreviewFromPlan(plan) {
|
|
113
|
+
const duplicateItems = plan.nodes
|
|
114
|
+
.filter((node) => node.duplicate !== null)
|
|
115
|
+
.map((node) => node.duplicate);
|
|
116
|
+
const exactDuplicateCandidates = duplicateItems.filter((item) => item.matchType === "exact").length;
|
|
117
|
+
const skippedOriginalIds = new Set(plan.options.duplicateMode === "skip_exact"
|
|
118
|
+
? plan.nodes.filter((node) => node.duplicate?.matchType === "exact" && node.originalId).map((node) => node.originalId)
|
|
119
|
+
: []);
|
|
120
|
+
const skippedNodes = plan.options.duplicateMode === "skip_exact"
|
|
121
|
+
? plan.nodes.filter((node) => node.duplicate?.matchType === "exact").length
|
|
122
|
+
: 0;
|
|
123
|
+
const skippedRelations = plan.options.duplicateMode === "skip_exact"
|
|
124
|
+
? plan.relations.filter((relation) => (relation.fromOriginalId && skippedOriginalIds.has(relation.fromOriginalId)) ||
|
|
125
|
+
(relation.toOriginalId && skippedOriginalIds.has(relation.toOriginalId))).length
|
|
126
|
+
: 0;
|
|
127
|
+
const skippedActivities = plan.options.duplicateMode === "skip_exact"
|
|
128
|
+
? plan.activities.filter((activity) => activity.targetOriginalId && skippedOriginalIds.has(activity.targetOriginalId)).length
|
|
129
|
+
: 0;
|
|
130
|
+
return {
|
|
131
|
+
format: plan.format,
|
|
132
|
+
label: plan.label,
|
|
133
|
+
sourcePath: plan.sourcePath,
|
|
134
|
+
createdAt: plan.createdAt,
|
|
135
|
+
options: plan.options,
|
|
136
|
+
nodesDetected: plan.nodes.length,
|
|
137
|
+
relationsDetected: plan.relations.length,
|
|
138
|
+
activitiesDetected: plan.activities.length,
|
|
139
|
+
duplicateCandidates: duplicateItems.length,
|
|
140
|
+
exactDuplicateCandidates,
|
|
141
|
+
nodesReady: plan.nodes.length - skippedNodes,
|
|
142
|
+
relationsReady: plan.relations.length - skippedRelations,
|
|
143
|
+
activitiesReady: plan.activities.length - skippedActivities,
|
|
144
|
+
skippedNodes,
|
|
145
|
+
skippedRelations,
|
|
146
|
+
skippedActivities,
|
|
147
|
+
warnings: [
|
|
148
|
+
...plan.warnings,
|
|
149
|
+
...(duplicateItems.length
|
|
150
|
+
? [`Detected ${duplicateItems.length} likely duplicate node(s) in the current import preview.`]
|
|
151
|
+
: []),
|
|
152
|
+
],
|
|
153
|
+
sampleItems: plan.nodes.slice(0, 5).map((node) => ({
|
|
154
|
+
title: node.title,
|
|
155
|
+
type: node.type,
|
|
156
|
+
sourcePath: node.sourcePath,
|
|
157
|
+
duplicateKind: node.duplicate?.matchType ?? null,
|
|
158
|
+
})),
|
|
159
|
+
duplicateItems,
|
|
160
|
+
};
|
|
161
|
+
}
|