recallx 1.0.8 → 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 +52 -1
- package/app/cli/src/cli.js +32 -1
- package/app/cli/src/format.js +24 -0
- package/app/mcp/index.js +2 -2
- package/app/mcp/server.js +523 -146
- package/app/server/app.js +413 -5
- package/app/server/config.js +4 -2
- package/app/server/index.js +12 -1
- package/app/server/observability.js +2 -0
- package/app/server/project-graph.js +13 -6
- package/app/server/repositories.js +178 -24
- 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 +119 -7
- package/app/shared/contracts.js +41 -0
- package/app/shared/version.js +1 -1
- package/dist/renderer/assets/{ProjectGraphCanvas-BLmjIT0R.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-C2-KXqBO.css +0 -1
- package/dist/renderer/assets/index-CIY8bKYQ.js +0 -69
|
@@ -59,12 +59,14 @@ function collectSearchFieldSignals(matcher, candidates) {
|
|
|
59
59
|
matchedFields: [],
|
|
60
60
|
exactFields: [],
|
|
61
61
|
matchedTermCount: 0,
|
|
62
|
+
matchedTermCounts: {},
|
|
62
63
|
totalTermCount: 0
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
const matchedFields = new Set();
|
|
66
67
|
const exactFields = new Set();
|
|
67
68
|
const matchedTerms = new Set();
|
|
69
|
+
const matchedTermCounts = {};
|
|
68
70
|
for (const candidate of candidates) {
|
|
69
71
|
const haystack = normalizeSearchText(candidate.value);
|
|
70
72
|
if (!haystack) {
|
|
@@ -76,6 +78,7 @@ function collectSearchFieldSignals(matcher, candidates) {
|
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
78
80
|
matchedFields.add(candidate.field);
|
|
81
|
+
matchedTermCounts[candidate.field] = termMatches.length;
|
|
79
82
|
if (exactMatch) {
|
|
80
83
|
exactFields.add(candidate.field);
|
|
81
84
|
}
|
|
@@ -87,6 +90,7 @@ function collectSearchFieldSignals(matcher, candidates) {
|
|
|
87
90
|
matchedFields: [...matchedFields],
|
|
88
91
|
exactFields: [...exactFields],
|
|
89
92
|
matchedTermCount: matchedTerms.size,
|
|
93
|
+
matchedTermCounts,
|
|
90
94
|
totalTermCount: matcher.matchTerms.length
|
|
91
95
|
};
|
|
92
96
|
}
|
|
@@ -94,14 +98,18 @@ function classifyNodeLexicalQuality(strategy, signals) {
|
|
|
94
98
|
if (strategy === "browse" || strategy === "semantic" || !signals.matchedFields.length) {
|
|
95
99
|
return "none";
|
|
96
100
|
}
|
|
97
|
-
|
|
98
|
-
return "weak";
|
|
99
|
-
}
|
|
100
|
-
const strongExactFields = new Set(["title", "summary", "tags"]);
|
|
101
|
+
const strongExactFields = new Set(["title", "summary", "tags", "body"]);
|
|
101
102
|
if (signals.exactFields.some((field) => strongExactFields.has(field))) {
|
|
102
103
|
return "strong";
|
|
103
104
|
}
|
|
104
105
|
const termCoverage = signals.totalTermCount > 0 ? signals.matchedTermCount / signals.totalTermCount : 0;
|
|
106
|
+
const titleCoverage = signals.totalTermCount > 0 ? (signals.matchedTermCounts.title ?? 0) / signals.totalTermCount : 0;
|
|
107
|
+
if (strategy === "fallback_token") {
|
|
108
|
+
return titleCoverage >= 0.5 ? "strong" : "weak";
|
|
109
|
+
}
|
|
110
|
+
if (strategy === "fts" && titleCoverage >= 0.5) {
|
|
111
|
+
return "strong";
|
|
112
|
+
}
|
|
105
113
|
if (strategy === "fts" && termCoverage >= 0.6 && signals.matchedFields.some((field) => strongExactFields.has(field))) {
|
|
106
114
|
return "strong";
|
|
107
115
|
}
|
|
@@ -1282,6 +1290,12 @@ export class RecallXRepository {
|
|
|
1282
1290
|
tags: parseJson(row.tags_json, [])
|
|
1283
1291
|
}));
|
|
1284
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
|
+
}
|
|
1285
1299
|
listActiveNodesByType(type, limit = 20) {
|
|
1286
1300
|
const rows = this.db
|
|
1287
1301
|
.prepare(`SELECT *
|
|
@@ -1586,37 +1600,60 @@ export class RecallXRepository {
|
|
|
1586
1600
|
const queryPresent = Boolean(input.query.trim());
|
|
1587
1601
|
const searchSort = input.sort === "smart" ? (queryPresent ? "relevance" : "updated_at") : input.sort;
|
|
1588
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
|
+
};
|
|
1589
1609
|
const nodeResults = includeNodes
|
|
1590
|
-
?
|
|
1610
|
+
? await runSearchStage("workspace.search.nodes.deterministic", {
|
|
1611
|
+
queryPresent,
|
|
1612
|
+
limit: requestedWindow,
|
|
1613
|
+
sort: searchSort
|
|
1614
|
+
}, () => this.searchNodes({
|
|
1591
1615
|
query: input.query,
|
|
1592
1616
|
filters: input.nodeFilters ?? {},
|
|
1593
1617
|
limit: requestedWindow,
|
|
1594
1618
|
offset: 0,
|
|
1595
1619
|
sort: searchSort
|
|
1596
|
-
})
|
|
1620
|
+
}))
|
|
1597
1621
|
: { items: [], total: 0 };
|
|
1598
1622
|
const activityResults = includeActivities
|
|
1599
|
-
?
|
|
1623
|
+
? await runSearchStage("workspace.search.activities.deterministic", {
|
|
1624
|
+
queryPresent,
|
|
1625
|
+
limit: requestedWindow,
|
|
1626
|
+
sort: searchSort
|
|
1627
|
+
}, () => this.searchActivities({
|
|
1600
1628
|
query: input.query,
|
|
1601
1629
|
filters: input.activityFilters ?? {},
|
|
1602
1630
|
limit: requestedWindow,
|
|
1603
1631
|
offset: 0,
|
|
1604
1632
|
sort: searchSort
|
|
1605
|
-
})
|
|
1633
|
+
}))
|
|
1606
1634
|
: { items: [], total: 0 };
|
|
1607
1635
|
const fallbackTriggered = queryPresent && nodeResults.total + activityResults.total === 0;
|
|
1608
1636
|
const fallbackTokens = fallbackTriggered ? tokenizeSearchQuery(input.query, SEARCH_FALLBACK_TOKEN_LIMIT) : [];
|
|
1609
1637
|
const resolvedNodeResults = fallbackTokens.length >= 2 && includeNodes
|
|
1610
|
-
?
|
|
1638
|
+
? await runSearchStage("workspace.search.nodes.fallback_token", {
|
|
1639
|
+
fallbackTokenCount: fallbackTokens.length,
|
|
1640
|
+
limit: requestedWindow,
|
|
1641
|
+
queryPresent
|
|
1642
|
+
}, () => this.searchWorkspaceNodeFallback(fallbackTokens, input.nodeFilters ?? {}, requestedWindow))
|
|
1611
1643
|
: nodeResults;
|
|
1612
1644
|
const resolvedActivityResults = fallbackTokens.length >= 2 && includeActivities
|
|
1613
|
-
?
|
|
1645
|
+
? await runSearchStage("workspace.search.activities.fallback_token", {
|
|
1646
|
+
fallbackTokenCount: fallbackTokens.length,
|
|
1647
|
+
limit: requestedWindow,
|
|
1648
|
+
queryPresent
|
|
1649
|
+
}, () => this.searchWorkspaceActivityFallback(fallbackTokens, input.activityFilters ?? {}, requestedWindow))
|
|
1614
1650
|
: activityResults;
|
|
1615
1651
|
const bestNodeLexicalQuality = summarizeLexicalQuality(resolvedNodeResults.items);
|
|
1616
1652
|
const bestActivityLexicalQuality = summarizeLexicalQuality(resolvedActivityResults.items);
|
|
1617
1653
|
const merged = this.mergeWorkspaceSearchResults(resolvedNodeResults.items, resolvedActivityResults.items, input.sort);
|
|
1654
|
+
const fallbackTokenUsed = fallbackTokens.length >= 2;
|
|
1618
1655
|
const deterministicResult = {
|
|
1619
|
-
total:
|
|
1656
|
+
total: fallbackTokenUsed
|
|
1620
1657
|
? merged.length
|
|
1621
1658
|
: resolvedNodeResults.total + resolvedActivityResults.total,
|
|
1622
1659
|
items: merged.slice(input.offset, input.offset + input.limit)
|
|
@@ -1639,15 +1676,23 @@ export class RecallXRepository {
|
|
|
1639
1676
|
const activityItems = result.items.flatMap((item) => item.resultType === "activity" && item.activity ? [item.activity] : []);
|
|
1640
1677
|
appendCurrentTelemetryDetails({
|
|
1641
1678
|
searchHit: result.items.length > 0,
|
|
1679
|
+
requestedWindow,
|
|
1642
1680
|
candidateCount: requestedWindow,
|
|
1681
|
+
rawNodeTotalCount: nodeResults.total,
|
|
1682
|
+
rawActivityTotalCount: activityResults.total,
|
|
1683
|
+
resolvedNodeTotalCount: resolvedNodeResults.total,
|
|
1684
|
+
resolvedActivityTotalCount: resolvedActivityResults.total,
|
|
1643
1685
|
nodeCandidateCount: resolvedNodeResults.items.length,
|
|
1644
1686
|
activityCandidateCount: resolvedActivityResults.items.length,
|
|
1687
|
+
mergedCandidateCount: merged.length,
|
|
1645
1688
|
nodeResultCount: nodeItems.length,
|
|
1646
1689
|
activityResultCount: activityItems.length,
|
|
1647
1690
|
bestNodeLexicalQuality,
|
|
1648
1691
|
bestActivityLexicalQuality,
|
|
1649
1692
|
lexicalNodeHit: bestNodeLexicalQuality !== "none",
|
|
1650
1693
|
strongNodeLexicalHit: bestNodeLexicalQuality === "strong",
|
|
1694
|
+
queryFallbackTriggered: fallbackTriggered,
|
|
1695
|
+
queryFallbackUsed: fallbackTokenUsed,
|
|
1651
1696
|
resultComposition: computeWorkspaceResultComposition({
|
|
1652
1697
|
nodeCount: nodeItems.length,
|
|
1653
1698
|
activityCount: activityItems.length,
|
|
@@ -1655,7 +1700,8 @@ export class RecallXRepository {
|
|
|
1655
1700
|
}),
|
|
1656
1701
|
resultCount: result.items.length,
|
|
1657
1702
|
totalCount: result.total,
|
|
1658
|
-
|
|
1703
|
+
queryFallbackTermCount: fallbackTokens.length,
|
|
1704
|
+
totalCountStrategy: fallbackTokenUsed ? "merged_length" : "source_total_sum",
|
|
1659
1705
|
semanticFallbackEligible: telemetry.semanticFallbackEligible,
|
|
1660
1706
|
semanticFallbackAttempted: telemetry.semanticFallbackAttempted,
|
|
1661
1707
|
semanticFallbackUsed: telemetry.semanticFallbackUsed,
|
|
@@ -2039,6 +2085,7 @@ export class RecallXRepository {
|
|
|
2039
2085
|
a.body,
|
|
2040
2086
|
a.source_label,
|
|
2041
2087
|
a.created_at,
|
|
2088
|
+
a.metadata_json,
|
|
2042
2089
|
n.title AS target_title,
|
|
2043
2090
|
n.type AS target_type,
|
|
2044
2091
|
n.status AS target_status
|
|
@@ -2066,6 +2113,7 @@ export class RecallXRepository {
|
|
|
2066
2113
|
body: row.body ? String(row.body) : null,
|
|
2067
2114
|
sourceLabel: row.source_label ? String(row.source_label) : null,
|
|
2068
2115
|
createdAt: String(row.created_at),
|
|
2116
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
2069
2117
|
lexicalQuality,
|
|
2070
2118
|
matchReason: buildSearchMatchReason(params.strategy, signals.matchedFields, {
|
|
2071
2119
|
strength: lexicalQuality === "none" ? undefined : lexicalQuality,
|
|
@@ -2312,25 +2360,59 @@ export class RecallXRepository {
|
|
|
2312
2360
|
: "";
|
|
2313
2361
|
const rows = this.db
|
|
2314
2362
|
.prepare(`SELECT
|
|
2315
|
-
r
|
|
2316
|
-
|
|
2363
|
+
r.id,
|
|
2364
|
+
r.from_node_id,
|
|
2365
|
+
r.to_node_id,
|
|
2366
|
+
r.relation_type,
|
|
2367
|
+
r.status,
|
|
2368
|
+
r.created_by,
|
|
2369
|
+
r.source_type,
|
|
2370
|
+
r.source_label,
|
|
2371
|
+
r.created_at,
|
|
2372
|
+
r.metadata_json,
|
|
2373
|
+
n.id AS node_id,
|
|
2374
|
+
n.type AS node_type,
|
|
2375
|
+
n.status AS node_status,
|
|
2376
|
+
n.canonicality AS node_canonicality,
|
|
2377
|
+
n.visibility AS node_visibility,
|
|
2378
|
+
n.title AS node_title,
|
|
2379
|
+
n.body AS node_body,
|
|
2380
|
+
n.summary AS node_summary,
|
|
2381
|
+
n.created_by AS node_created_by,
|
|
2382
|
+
n.source_type AS node_source_type,
|
|
2383
|
+
n.source_label AS node_source_label,
|
|
2384
|
+
n.created_at AS node_created_at,
|
|
2385
|
+
n.updated_at AS node_updated_at,
|
|
2386
|
+
n.tags_json AS node_tags_json,
|
|
2387
|
+
n.metadata_json AS node_metadata_json
|
|
2317
2388
|
FROM relations r
|
|
2389
|
+
JOIN nodes n
|
|
2390
|
+
ON n.id = CASE WHEN r.from_node_id = ? THEN r.to_node_id ELSE r.from_node_id END
|
|
2318
2391
|
WHERE (r.from_node_id = ? OR r.to_node_id = ?)
|
|
2319
2392
|
AND r.status != 'archived'
|
|
2320
2393
|
${relationWhere}
|
|
2321
2394
|
ORDER BY r.created_at DESC`)
|
|
2322
2395
|
.all(nodeId, nodeId, nodeId, ...(relationFilter ?? []));
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2396
|
+
return rows.map((row) => ({
|
|
2397
|
+
relation: mapRelation(row),
|
|
2398
|
+
node: {
|
|
2399
|
+
id: String(row.node_id),
|
|
2400
|
+
type: row.node_type,
|
|
2401
|
+
status: row.node_status,
|
|
2402
|
+
canonicality: row.node_canonicality,
|
|
2403
|
+
visibility: String(row.node_visibility),
|
|
2404
|
+
title: row.node_title ? String(row.node_title) : null,
|
|
2405
|
+
body: row.node_body ? String(row.node_body) : null,
|
|
2406
|
+
summary: row.node_summary ? String(row.node_summary) : null,
|
|
2407
|
+
createdBy: row.node_created_by ? String(row.node_created_by) : null,
|
|
2408
|
+
sourceType: row.node_source_type ? String(row.node_source_type) : null,
|
|
2409
|
+
sourceLabel: row.node_source_label ? String(row.node_source_label) : null,
|
|
2410
|
+
createdAt: String(row.node_created_at),
|
|
2411
|
+
updatedAt: String(row.node_updated_at),
|
|
2412
|
+
tags: parseJson(row.node_tags_json, []),
|
|
2413
|
+
metadata: parseJson(row.node_metadata_json, {})
|
|
2328
2414
|
}
|
|
2329
|
-
|
|
2330
|
-
relation: mapRelation(row),
|
|
2331
|
-
node
|
|
2332
|
-
}];
|
|
2333
|
-
});
|
|
2415
|
+
}));
|
|
2334
2416
|
}
|
|
2335
2417
|
listProjectMemberNodes(projectId, limit) {
|
|
2336
2418
|
const rows = this.db
|
|
@@ -2375,6 +2457,12 @@ export class RecallXRepository {
|
|
|
2375
2457
|
.all(...uniqueIds, ...uniqueIds);
|
|
2376
2458
|
return rows.map(mapRelation);
|
|
2377
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
|
+
}
|
|
2378
2466
|
createRelation(input) {
|
|
2379
2467
|
const now = nowIso();
|
|
2380
2468
|
const id = createId("rel");
|
|
@@ -2678,6 +2766,60 @@ export class RecallXRepository {
|
|
|
2678
2766
|
.all(entityType, entityId, limit);
|
|
2679
2767
|
return rows.map(mapGovernanceEvent);
|
|
2680
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
|
+
}
|
|
2681
2823
|
upsertGovernanceState(params) {
|
|
2682
2824
|
const now = params.lastEvaluatedAt ?? nowIso();
|
|
2683
2825
|
const existing = params.previousState === undefined
|
|
@@ -2986,6 +3128,12 @@ export class RecallXRepository {
|
|
|
2986
3128
|
.all(...uniqueIds, limit);
|
|
2987
3129
|
return rows.map(mapActivity);
|
|
2988
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
|
+
}
|
|
2989
3137
|
appendActivity(input) {
|
|
2990
3138
|
const id = createId("act");
|
|
2991
3139
|
const now = nowIso();
|
|
@@ -3068,6 +3216,12 @@ export class RecallXRepository {
|
|
|
3068
3216
|
.all(nodeId);
|
|
3069
3217
|
return rows.map(mapArtifact);
|
|
3070
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
|
+
}
|
|
3071
3225
|
getArtifact(id) {
|
|
3072
3226
|
const row = this.db.prepare(`SELECT * FROM artifacts WHERE id = ?`).get(id);
|
|
3073
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
|
+
}
|