dsh-codegraph-visualizer 0.1.0 → 0.3.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 +391 -223
- package/assets/icon-180.png +0 -0
- package/assets/icon.svg +30 -0
- package/cordis.patch.yml +22 -17
- package/dist/CodeGraphAdapter-C8JvcS7m.js +10 -0
- package/dist/CodeGraphAdapter-eyrhs0K0.js +422 -0
- package/dist/CodeGraphAdapter-eyrhs0K0.js.map +1 -0
- package/dist/FileSystemAdapter-CgasSGbB.js +524 -0
- package/dist/FileSystemAdapter-CgasSGbB.js.map +1 -0
- package/dist/LensAdapter-C9Saf_i_.js +89 -0
- package/dist/LensAdapter-C9Saf_i_.js.map +1 -0
- package/dist/Logger-DUIJSeNu.js +80 -0
- package/dist/Logger-DUIJSeNu.js.map +1 -0
- package/dist/TreeSitterAdapter-C-EWy8br.js +631 -0
- package/dist/TreeSitterAdapter-C-EWy8br.js.map +1 -0
- package/dist/TreeSitterAdapter-C6FcO2_s.js +8 -0
- package/dist/client/index.css +8900 -1777
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.d.ts +634 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.esm.js +57219 -20978
- package/dist/client/index.js.map +1 -0
- package/dist/client/{index.js → worker/layoutWorker.js} +7657 -8787
- package/dist/client.js +90067 -0
- package/dist/codeGraphDb-BT9nZCEO.js +357 -0
- package/dist/codeGraphDb-BT9nZCEO.js.map +1 -0
- package/dist/codeGraphDb-Bg7bWud2.js +9 -0
- package/dist/codeGraphRows-BhZX34uB.js +6 -0
- package/dist/codeGraphRows-C7hqu35E.js +246 -0
- package/dist/codeGraphRows-C7hqu35E.js.map +1 -0
- package/dist/constants-CQq93l3e.js +399 -0
- package/dist/constants-CQq93l3e.js.map +1 -0
- package/dist/graphFingerprint-CeARo7jG.js +4 -0
- package/dist/graphFingerprint-PuxORZUF.js +277 -0
- package/dist/graphFingerprint-PuxORZUF.js.map +1 -0
- package/dist/graphMetrics-C0F30730.js +555 -0
- package/dist/graphMetrics-C0F30730.js.map +1 -0
- package/dist/index.d.ts +572 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14734 -2003
- package/dist/index.js.map +1 -1
- package/dist/inheritanceEnhancer-Bi3UoYJS.js +291 -0
- package/dist/inheritanceEnhancer-Bi3UoYJS.js.map +1 -0
- package/dist/layerUtils-DDm-EsaD.js +72 -0
- package/dist/layerUtils-DDm-EsaD.js.map +1 -0
- package/dist/nativeIndex-Cx7RLXUp.js +5 -0
- package/dist/nativeIndex-DlSpD611.js +347 -0
- package/dist/nativeIndex-DlSpD611.js.map +1 -0
- package/dist/treeSitterExtractors-B1ybKNrC.js +394 -0
- package/dist/treeSitterExtractors-B1ybKNrC.js.map +1 -0
- package/dist/types-BUDxI_00.js +23 -0
- package/dist/types-BUDxI_00.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +101 -21
- package/dist/client-cjs/index.cjs +0 -44283
- package/dist/client-cjs/index.css +0 -2404
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="Code Graph Visualizer">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="cg-bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#4f46e5"/>
|
|
5
|
+
<stop offset="1" stop-color="#7c3aed"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="128" height="128" rx="28" fill="url(#cg-bg)"/>
|
|
9
|
+
<g stroke="#ffffff" stroke-opacity="0.72" stroke-width="3.5" stroke-linecap="round" fill="none">
|
|
10
|
+
<path d="M64 34 L40 44"/>
|
|
11
|
+
<path d="M64 34 L88 46"/>
|
|
12
|
+
<path d="M64 34 L64 62"/>
|
|
13
|
+
<path d="M40 44 L34 78"/>
|
|
14
|
+
<path d="M88 46 L94 80"/>
|
|
15
|
+
<path d="M40 44 L64 62"/>
|
|
16
|
+
<path d="M88 46 L64 62"/>
|
|
17
|
+
<path d="M34 78 L64 96"/>
|
|
18
|
+
<path d="M94 80 L64 96"/>
|
|
19
|
+
<path d="M64 62 L64 96"/>
|
|
20
|
+
</g>
|
|
21
|
+
<g fill="#ffffff">
|
|
22
|
+
<circle cx="64" cy="34" r="8.5"/>
|
|
23
|
+
<circle cx="40" cy="44" r="7"/>
|
|
24
|
+
<circle cx="88" cy="46" r="7"/>
|
|
25
|
+
<circle cx="34" cy="78" r="7"/>
|
|
26
|
+
<circle cx="94" cy="80" r="7"/>
|
|
27
|
+
<circle cx="64" cy="96" r="8.5"/>
|
|
28
|
+
</g>
|
|
29
|
+
<circle cx="64" cy="62" r="7.5" fill="#c7d2fe"/>
|
|
30
|
+
</svg>
|
package/cordis.patch.yml
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
# CodeGraph Visualizer bundle patch
|
|
2
|
-
# Applied after dsh-base and dsh-web-app layers; inserts this plugin as a
|
|
3
|
-
# host-plane row so its graph_* tools register into the global `tools` registry.
|
|
4
|
-
# Config is explicit (red line 9) — all fields below match DEFAULT_CONFIG in
|
|
5
|
-
# src/index.ts; misconfigured values fail loud at load.
|
|
6
|
-
- insert:
|
|
7
|
-
- id: codegraph-visualizer
|
|
8
|
-
name: dsh-codegraph-visualizer
|
|
9
|
-
config:
|
|
10
|
-
dataSource: auto
|
|
11
|
-
requestTimeout:
|
|
12
|
-
scanCacheTtl: 30000
|
|
13
|
-
scanCacheLimit: 4
|
|
14
|
-
maxBodyBytes: 1048576
|
|
15
|
-
prerequisiteRetryDelay: 3000
|
|
16
|
-
watchDebounce: 500
|
|
17
|
-
maxNodes:
|
|
1
|
+
# CodeGraph Visualizer bundle patch
|
|
2
|
+
# Applied after dsh-base and dsh-web-app layers; inserts this plugin as a
|
|
3
|
+
# host-plane row so its graph_* tools register into the global `tools` registry.
|
|
4
|
+
# Config is explicit (red line 9) — all fields below match DEFAULT_CONFIG in
|
|
5
|
+
# src/index.ts; misconfigured values fail loud at load.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: codegraph-visualizer
|
|
8
|
+
name: dsh-codegraph-visualizer
|
|
9
|
+
config:
|
|
10
|
+
dataSource: auto
|
|
11
|
+
requestTimeout: 15000
|
|
12
|
+
scanCacheTtl: 30000
|
|
13
|
+
scanCacheLimit: 4
|
|
14
|
+
maxBodyBytes: 1048576
|
|
15
|
+
prerequisiteRetryDelay: 3000
|
|
16
|
+
watchDebounce: 500
|
|
17
|
+
maxNodes: 500000
|
|
18
|
+
allowedOrigins:
|
|
19
|
+
- http://localhost:3080
|
|
20
|
+
- http://127.0.0.1:3080
|
|
21
|
+
panelMinHeight: 240
|
|
22
|
+
panelDefaultHeight: 600
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./types-BUDxI_00.js";
|
|
2
|
+
import "./constants-CQq93l3e.js";
|
|
3
|
+
import "./Logger-DUIJSeNu.js";
|
|
4
|
+
import "./layerUtils-DDm-EsaD.js";
|
|
5
|
+
import { EDGE_ROW_COLUMNS, FILTERED_EDGE_KINDS, isNoisePath, mapEdgeKind, mapNodeKind, mapRowsToGraphEdges, mapRowsToGraphNodes, normalizeRepoPath, parseEdgeConfidence } from "./codeGraphRows-C7hqu35E.js";
|
|
6
|
+
import "./nativeIndex-DlSpD611.js";
|
|
7
|
+
import { closeAllDbConnections, countEdgesInDb, readFileOutlineFromDb, readGraphFromDb, readGraphFromDbPaged, readNodeChildrenFromDb, readSymbolASTFromDb, readTopLevelNodesFromDb } from "./codeGraphDb-BT9nZCEO.js";
|
|
8
|
+
import { CODEGRAPH_SOURCE, CodeGraphAdapter, readEdgesForNodesFromDb, readEdgesFromDb, readFilesFromDb, readIncidentEdgesWithFilePathsFromDb, resolveDbPath, sortEdgesByKindGroup } from "./CodeGraphAdapter-eyrhs0K0.js";
|
|
9
|
+
|
|
10
|
+
export { CODEGRAPH_SOURCE, CodeGraphAdapter, EDGE_ROW_COLUMNS, FILTERED_EDGE_KINDS, closeAllDbConnections, countEdgesInDb, isNoisePath, mapEdgeKind, mapNodeKind, mapRowsToGraphEdges, mapRowsToGraphNodes, normalizeRepoPath, parseEdgeConfidence, readEdgesForNodesFromDb, readEdgesFromDb, readFileOutlineFromDb, readFilesFromDb, readGraphFromDb, readGraphFromDbPaged, readIncidentEdgesWithFilePathsFromDb, readNodeChildrenFromDb, readSymbolASTFromDb, readTopLevelNodesFromDb, resolveDbPath, sortEdgesByKindGroup };
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { NodeId } from "./types-BUDxI_00.js";
|
|
2
|
+
import { INCREMENTAL, SQLITE } from "./constants-CQq93l3e.js";
|
|
3
|
+
import { scoped } from "./Logger-DUIJSeNu.js";
|
|
4
|
+
import { inferLayerFromPath } from "./layerUtils-DDm-EsaD.js";
|
|
5
|
+
import { EDGE_ROW_COLUMNS, FILTERED_EDGE_KINDS, isNoisePath, mapEdgeKind, mapRowsToGraphEdges, mapRowsToGraphNodes, normalizeRepoPath } from "./codeGraphRows-C7hqu35E.js";
|
|
6
|
+
import { collapseRepeatedBackslashes } from "./nativeIndex-DlSpD611.js";
|
|
7
|
+
import { getDbConnection, readGraphFromDbPaged, validateEdgeRows, validateNodeRows } from "./codeGraphDb-BT9nZCEO.js";
|
|
8
|
+
import { join, normalize } from "node:path";
|
|
9
|
+
|
|
10
|
+
//#region src/adapters/codeGraphEdgeReads.ts
|
|
11
|
+
const log = scoped("codegraph-adapter");
|
|
12
|
+
/**
|
|
13
|
+
|
|
14
|
+
* file 域边列表的 kind 分组排序(cycle-139 / 需求分析 §82)。
|
|
15
|
+
|
|
16
|
+
*
|
|
17
|
+
|
|
18
|
+
* ## 它解决什么
|
|
19
|
+
|
|
20
|
+
*
|
|
21
|
+
|
|
22
|
+
* `/edges` 的 file 域返回「某文件的全部邻接边」。SQLite 无 ORDER BY(id 序返回),
|
|
23
|
+
|
|
24
|
+
* 客户端拿到的边顺序是**索引插入序**——call/import/contains 混排,用户在边列表里
|
|
25
|
+
|
|
26
|
+
* 找「谁调用了这个文件」要在几百条杂序边里翻。分组排序后同 kind 的边相邻,
|
|
27
|
+
|
|
28
|
+
* 边列表呈现为可扫读的段(call 段 → import 段 → …)。
|
|
29
|
+
|
|
30
|
+
*
|
|
31
|
+
|
|
32
|
+
* ## 契约
|
|
33
|
+
|
|
34
|
+
*
|
|
35
|
+
|
|
36
|
+
* - **组序**:call → import → extend → implement → contains → dependency,
|
|
37
|
+
|
|
38
|
+
* 未知/其他 kind 按 `mapEdgeKind` 归并后的字母序殿后(新 kind 不需要改本表,
|
|
39
|
+
|
|
40
|
+
* 但会排到已知段之后——确定、可预期)。
|
|
41
|
+
|
|
42
|
+
* - **组内序**:`(source, target, id)` 字典序——纯确定序,与 DB 返回序无关。
|
|
43
|
+
|
|
44
|
+
* - **排序在过滤之后、切片之前**:与「先过滤再切片」既有契约同位(total 是过滤后
|
|
45
|
+
|
|
46
|
+
* 计数;排序改变的是序不是集合,不改变 total)。
|
|
47
|
+
|
|
48
|
+
* - **两条数据路径共用**:DB-first(`readEdgesFromDb` file 域 / 聚合路径的未切片全集)
|
|
49
|
+
|
|
50
|
+
* 与内存回退各自调用本函数,不各写一份(「同语义共享一份实现」纪律)。
|
|
51
|
+
|
|
52
|
+
* 组键兼容 `DbEdgeRow.kind` 与 `GraphEdge.type` 两种来源(见函数注)。
|
|
53
|
+
|
|
54
|
+
*/
|
|
55
|
+
const EDGE_KIND_GROUP_ORDER = [
|
|
56
|
+
"call",
|
|
57
|
+
"import",
|
|
58
|
+
"extend",
|
|
59
|
+
"implement",
|
|
60
|
+
"contains",
|
|
61
|
+
"dependency"
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
|
|
65
|
+
* 组键取值:`DbEdgeRow.kind`(DB 侧原始 kind)**或** `GraphEdge.type`(内存回落侧
|
|
66
|
+
|
|
67
|
+
* 已映射的图类型)。两条路径的字段名不同——只读其一会让另一条路径的全部边落进
|
|
68
|
+
|
|
69
|
+
* dependency 组、排序静默退化为坐标序(本轮实测踩坑:测试当场抓住)。
|
|
70
|
+
|
|
71
|
+
*/
|
|
72
|
+
function sortEdgesByKindGroup(edges) {
|
|
73
|
+
const groupOf = (e) => mapEdgeKind(e.kind ?? e.type ?? "dependency");
|
|
74
|
+
const groupRank = new Map();
|
|
75
|
+
EDGE_KIND_GROUP_ORDER.forEach((kind, i) => groupRank.set(kind, i));
|
|
76
|
+
return [...edges].sort((a, b) => {
|
|
77
|
+
const ga = groupOf(a);
|
|
78
|
+
const gb = groupOf(b);
|
|
79
|
+
if (ga !== gb) {
|
|
80
|
+
const ra = groupRank.get(ga);
|
|
81
|
+
const rb = groupRank.get(gb);
|
|
82
|
+
if (ra !== void 0 || rb !== void 0) return (ra ?? EDGE_KIND_GROUP_ORDER.length) - (rb ?? EDGE_KIND_GROUP_ORDER.length);
|
|
83
|
+
return ga < gb ? -1 : ga > gb ? 1 : 0;
|
|
84
|
+
}
|
|
85
|
+
const sa = String(a.source);
|
|
86
|
+
const sb = String(b.source);
|
|
87
|
+
if (sa !== sb) return sa < sb ? -1 : 1;
|
|
88
|
+
const ta = String(a.target);
|
|
89
|
+
const tb = String(b.target);
|
|
90
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
91
|
+
const ia = String(a.id);
|
|
92
|
+
const ib = String(b.id);
|
|
93
|
+
return ia < ib ? -1 : ia > ib ? 1 : 0;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
|
|
98
|
+
* Paged read of a node's incident edges, already filtered by the shared edge
|
|
99
|
+
|
|
100
|
+
* policy (`FILTERED_EDGE_KINDS` + self-loops).
|
|
101
|
+
|
|
102
|
+
*
|
|
103
|
+
|
|
104
|
+
* The filter MUST run before slicing: `total` is the filtered count and the
|
|
105
|
+
|
|
106
|
+
* caller advances its cursor by the returned page length, so filtering after
|
|
107
|
+
|
|
108
|
+
* the slice would make `total` unreachable and re-read rows on every page.
|
|
109
|
+
|
|
110
|
+
* `CodeGraphAdapter.fetchData` applies the same policy to `dbEdges`.
|
|
111
|
+
|
|
112
|
+
*/
|
|
113
|
+
function readEdgesFromDb(dbPath, nodeId, scope, cursor, limit) {
|
|
114
|
+
const db = getDbConnection(dbPath);
|
|
115
|
+
if (!db) return null;
|
|
116
|
+
const keep = (e) => e.source !== e.target && !FILTERED_EDGE_KINDS.has(e.kind);
|
|
117
|
+
try {
|
|
118
|
+
if (scope === "symbol") {
|
|
119
|
+
const allEdges$1 = validateEdgeRows(db.prepare(`SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source = ? OR target = ?`).all(nodeId, nodeId)).filter(keep);
|
|
120
|
+
const total$1 = allEdges$1.length;
|
|
121
|
+
const pagedEdges$1 = allEdges$1.slice(cursor, cursor + limit);
|
|
122
|
+
return {
|
|
123
|
+
edges: pagedEdges$1,
|
|
124
|
+
total: total$1
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const fileResult = readIncidentEdgesWithFilePathsFromDb(dbPath, nodeId, "file");
|
|
128
|
+
if (!fileResult) return null;
|
|
129
|
+
const allEdges = sortEdgesByKindGroup(fileResult.edges);
|
|
130
|
+
const total = allEdges.length;
|
|
131
|
+
const pagedEdges = allEdges.slice(cursor, cursor + limit);
|
|
132
|
+
return {
|
|
133
|
+
edges: pagedEdges,
|
|
134
|
+
total
|
|
135
|
+
};
|
|
136
|
+
} catch (e) {
|
|
137
|
+
log.warn("readEdgesFromDb failed", e);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
|
|
143
|
+
* 一次性读回某节点(或其所在文件)的全部邻接边,**不切片**,并附上每个端点所属文件。
|
|
144
|
+
|
|
145
|
+
*
|
|
146
|
+
|
|
147
|
+
* 供「文件对聚合」使用:聚合必须在分页之前完成(否则每页各自聚合,
|
|
148
|
+
|
|
149
|
+
* 同一个文件对会被拆到多页里),所以聚合路径需要未切片的全集。
|
|
150
|
+
|
|
151
|
+
* 调用方拿到结果后自行决定聚合还是切片,因此这里刻意不接受 cursor/limit。
|
|
152
|
+
|
|
153
|
+
*
|
|
154
|
+
|
|
155
|
+
* 遵守与 `readEdgesFromDb` 相同的约束:分批 IN 查询只用于规避 SQLite 参数上限,
|
|
156
|
+
|
|
157
|
+
* 绝不在分批内使用 LIMIT/OFFSET(OFFSET 作用于整个结果集而非单个批次)。
|
|
158
|
+
|
|
159
|
+
*/
|
|
160
|
+
function readIncidentEdgesWithFilePathsFromDb(dbPath, nodeId, scope) {
|
|
161
|
+
const db = getDbConnection(dbPath);
|
|
162
|
+
if (!db) return null;
|
|
163
|
+
const keep = (e) => e.source !== e.target && !FILTERED_EDGE_KINDS.has(e.kind);
|
|
164
|
+
try {
|
|
165
|
+
const filePathByNodeId = new Map();
|
|
166
|
+
const edgeMap = new Map();
|
|
167
|
+
const batchSize = Math.floor(SQLITE.MAX_IN_PARAMS / 2);
|
|
168
|
+
if (scope === "file") {
|
|
169
|
+
const filePathResult = db.prepare("SELECT file_path FROM nodes WHERE id = ?").get(nodeId);
|
|
170
|
+
if (!filePathResult) return {
|
|
171
|
+
edges: [],
|
|
172
|
+
filePathByNodeId
|
|
173
|
+
};
|
|
174
|
+
const filePath = filePathResult.file_path;
|
|
175
|
+
const nodeIds = db.prepare("SELECT id FROM nodes WHERE file_path = ?").all(filePath);
|
|
176
|
+
const idList = nodeIds.map((n) => n.id);
|
|
177
|
+
if (idList.length === 0) return {
|
|
178
|
+
edges: [],
|
|
179
|
+
filePathByNodeId
|
|
180
|
+
};
|
|
181
|
+
for (const id of idList) filePathByNodeId.set(id, filePath);
|
|
182
|
+
for (let i = 0; i < idList.length; i += batchSize) {
|
|
183
|
+
const batch = idList.slice(i, i + batchSize);
|
|
184
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
185
|
+
const batchEdges = validateEdgeRows(db.prepare(`SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source IN (${placeholders}) OR target IN (${placeholders})`).all(...batch, ...batch));
|
|
186
|
+
for (const e of batchEdges) edgeMap.set(e.id, e);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const symbolEdges = validateEdgeRows(db.prepare(`SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source = ? OR target = ?`).all(nodeId, nodeId));
|
|
190
|
+
for (const e of symbolEdges) edgeMap.set(e.id, e);
|
|
191
|
+
}
|
|
192
|
+
const edges = [...edgeMap.values()].filter(keep);
|
|
193
|
+
const unresolved = new Set();
|
|
194
|
+
for (const e of edges) {
|
|
195
|
+
if (!filePathByNodeId.has(e.source)) unresolved.add(e.source);
|
|
196
|
+
if (!filePathByNodeId.has(e.target)) unresolved.add(e.target);
|
|
197
|
+
}
|
|
198
|
+
const unresolvedList = [...unresolved];
|
|
199
|
+
for (let i = 0; i < unresolvedList.length; i += batchSize) {
|
|
200
|
+
const batch = unresolvedList.slice(i, i + batchSize);
|
|
201
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
202
|
+
const rows = db.prepare(`SELECT id, file_path FROM nodes WHERE id IN (${placeholders})`).all(...batch);
|
|
203
|
+
for (const r of rows) filePathByNodeId.set(r.id, r.file_path);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
edges,
|
|
207
|
+
filePathByNodeId
|
|
208
|
+
};
|
|
209
|
+
} catch (e) {
|
|
210
|
+
log.warn("readIncidentEdgesWithFilePathsFromDb failed", e);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function readEdgesForNodesFromDb(dbPath, nodeIds) {
|
|
215
|
+
if (nodeIds.length === 0) return [];
|
|
216
|
+
const db = getDbConnection(dbPath);
|
|
217
|
+
if (!db) return [];
|
|
218
|
+
try {
|
|
219
|
+
const edgeMap = new Map();
|
|
220
|
+
const nodeSet = new Set();
|
|
221
|
+
for (const id of nodeIds) {
|
|
222
|
+
nodeSet.add(id);
|
|
223
|
+
const expanded = id.replace(/\\/g, "\\\\");
|
|
224
|
+
if (expanded !== id) nodeSet.add(expanded);
|
|
225
|
+
}
|
|
226
|
+
const batchSize = Math.floor(SQLITE.MAX_IN_PARAMS / 2);
|
|
227
|
+
for (let i = 0; i < nodeIds.length; i += batchSize) {
|
|
228
|
+
const batch = nodeIds.slice(i, i + batchSize);
|
|
229
|
+
const queryBatch = batch.flatMap((id) => {
|
|
230
|
+
const expanded = id.replace(/\\/g, "\\\\");
|
|
231
|
+
return expanded !== id ? [id, expanded] : [id];
|
|
232
|
+
});
|
|
233
|
+
const placeholders = queryBatch.map(() => "?").join(", ");
|
|
234
|
+
const edges = validateEdgeRows(db.prepare(`SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source IN (${placeholders}) OR target IN (${placeholders})`).all(...queryBatch, ...queryBatch));
|
|
235
|
+
for (const e of edges) if (nodeSet.has(e.source) && nodeSet.has(e.target)) edgeMap.set(e.id, e);
|
|
236
|
+
}
|
|
237
|
+
const result = [...edgeMap.values()];
|
|
238
|
+
for (const e of result) {
|
|
239
|
+
e.source = collapseRepeatedBackslashes(e.source);
|
|
240
|
+
e.target = collapseRepeatedBackslashes(e.target);
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
log.warn("readEdgesForNodesFromDb failed", e);
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
|
|
250
|
+
* Read the nodes of a set of changed files plus every edge that touches one of them (P2-21).
|
|
251
|
+
|
|
252
|
+
*
|
|
253
|
+
|
|
254
|
+
* Both separator spellings of each path are tried, and a changed *directory* is matched by prefix so
|
|
255
|
+
|
|
256
|
+
* that a file added under a new folder still pulls that folder's rows.
|
|
257
|
+
|
|
258
|
+
*
|
|
259
|
+
|
|
260
|
+
* Edges are returned when **either** endpoint is in the touched node set, not only when both are: an
|
|
261
|
+
|
|
262
|
+
* edge from a changed file into an unchanged one is exactly the kind of edge a re-scan can add or
|
|
263
|
+
|
|
264
|
+
* remove, and filtering it out here would make it invisible to the caller. The caller is responsible
|
|
265
|
+
|
|
266
|
+
* for dropping edges whose far endpoint no longer exists in the resulting graph.
|
|
267
|
+
|
|
268
|
+
*
|
|
269
|
+
|
|
270
|
+
* Returns `null` when the index cannot be read at all, which callers treat as "fall back to a full
|
|
271
|
+
|
|
272
|
+
* fetch"; an empty result means the index was read and holds nothing for those paths.
|
|
273
|
+
|
|
274
|
+
*/
|
|
275
|
+
function readFilesFromDb(dbPath, filePaths) {
|
|
276
|
+
if (filePaths.length === 0) return {
|
|
277
|
+
nodes: [],
|
|
278
|
+
edges: []
|
|
279
|
+
};
|
|
280
|
+
const db = getDbConnection(dbPath);
|
|
281
|
+
if (!db) return null;
|
|
282
|
+
try {
|
|
283
|
+
const wanted = new Set();
|
|
284
|
+
for (const raw of filePaths) {
|
|
285
|
+
const normalized = normalizeRepoPath(raw);
|
|
286
|
+
if (!normalized) continue;
|
|
287
|
+
wanted.add(normalized);
|
|
288
|
+
wanted.add(normalized.replace(/\//g, "\\"));
|
|
289
|
+
}
|
|
290
|
+
const candidates = [...wanted];
|
|
291
|
+
if (candidates.length === 0) return {
|
|
292
|
+
nodes: [],
|
|
293
|
+
edges: []
|
|
294
|
+
};
|
|
295
|
+
const batchSize = Math.min(INCREMENTAL.MAX_FILE_BATCH, Math.floor(SQLITE.MAX_IN_PARAMS / 2));
|
|
296
|
+
const nodeMap = new Map();
|
|
297
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
298
|
+
const batch = candidates.slice(i, i + batchSize);
|
|
299
|
+
const exact = batch.map(() => "?").join(",");
|
|
300
|
+
const prefix = batch.map(() => "file_path LIKE ? ESCAPE '\\'").join(" OR ");
|
|
301
|
+
const prefixArgs = batch.map((p) => `${p.replace(/[%_\\]/g, "\\$&")}/%`);
|
|
302
|
+
const rows = validateNodeRows(db.prepare(`SELECT id, kind, name, file_path, start_line, signature, docstring, is_exported FROM nodes
|
|
303
|
+
WHERE file_path IN (${exact}) OR ${prefix}`).all(...batch, ...prefixArgs));
|
|
304
|
+
for (const n of rows) nodeMap.set(n.id, n);
|
|
305
|
+
}
|
|
306
|
+
if (nodeMap.size === 0) return {
|
|
307
|
+
nodes: [],
|
|
308
|
+
edges: []
|
|
309
|
+
};
|
|
310
|
+
const nodeIds = [...nodeMap.keys()];
|
|
311
|
+
const edgeMap = new Map();
|
|
312
|
+
for (let i = 0; i < nodeIds.length; i += batchSize) {
|
|
313
|
+
const batch = nodeIds.slice(i, i + batchSize);
|
|
314
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
315
|
+
const rows = validateEdgeRows(db.prepare(`SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source IN (${placeholders}) OR target IN (${placeholders})`).all(...batch, ...batch));
|
|
316
|
+
for (const e of rows) edgeMap.set(e.id, e);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
nodes: [...nodeMap.values()],
|
|
320
|
+
edges: [...edgeMap.values()]
|
|
321
|
+
};
|
|
322
|
+
} catch (e) {
|
|
323
|
+
log.warn("readFilesFromDb failed", e);
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/adapters/CodeGraphAdapter.ts
|
|
330
|
+
/**
|
|
331
|
+
* Locate the .codegraph/codegraph.db for a workspace path.
|
|
332
|
+
* Callers MUST validate workspacePath via resolveWorkspaceFromRequest or
|
|
333
|
+
* validateWorkspacePath before calling this function.
|
|
334
|
+
*/
|
|
335
|
+
function resolveDbPath(workspacePath) {
|
|
336
|
+
const root = workspacePath && workspacePath !== "." ? workspacePath : process.cwd();
|
|
337
|
+
const normalized = normalize(root);
|
|
338
|
+
if (normalized.includes("..")) throw new Error(`Invalid workspace path: ${workspacePath}`);
|
|
339
|
+
return join(normalized, ".codegraph", "codegraph.db");
|
|
340
|
+
}
|
|
341
|
+
/** The `source` every row this adapter produces is attributed to. */
|
|
342
|
+
const CODEGRAPH_SOURCE = "codegraph";
|
|
343
|
+
var CodeGraphAdapter = class {
|
|
344
|
+
source = CODEGRAPH_SOURCE;
|
|
345
|
+
async fetchData(repoId, invoke) {
|
|
346
|
+
const dbPath = resolveDbPath(repoId);
|
|
347
|
+
let cursor = null;
|
|
348
|
+
const allNodes = [];
|
|
349
|
+
const allEdges = [];
|
|
350
|
+
const PAGE = 500;
|
|
351
|
+
do {
|
|
352
|
+
const page = readGraphFromDbPaged(dbPath, cursor, PAGE);
|
|
353
|
+
if (!page) {
|
|
354
|
+
allNodes.length = 0;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
allNodes.push(...page.nodes);
|
|
358
|
+
allEdges.push(...page.edges);
|
|
359
|
+
cursor = page.nextCursor;
|
|
360
|
+
} while (cursor !== null);
|
|
361
|
+
if (allNodes.length > 0) return this.toAdapterResult(allNodes, allEdges);
|
|
362
|
+
const files = await invoke("codegraph_files", {
|
|
363
|
+
path: repoId,
|
|
364
|
+
format: "flat"
|
|
365
|
+
});
|
|
366
|
+
if (!files || typeof files !== "object") return {
|
|
367
|
+
nodes: [],
|
|
368
|
+
edges: [],
|
|
369
|
+
source: this.source,
|
|
370
|
+
timestamp: Date.now()
|
|
371
|
+
};
|
|
372
|
+
const rawFiles = this.extractFileList(files);
|
|
373
|
+
const nodes = rawFiles.filter((f) => !isNoisePath(f.path)).map((f, i) => ({
|
|
374
|
+
id: NodeId(`file:${f.path}`),
|
|
375
|
+
label: f.path.split("/").pop() ?? f.path,
|
|
376
|
+
type: "module",
|
|
377
|
+
filePath: f.path,
|
|
378
|
+
lineNumber: 1,
|
|
379
|
+
properties: {
|
|
380
|
+
kind: "file",
|
|
381
|
+
language: f.language ?? null,
|
|
382
|
+
index: i
|
|
383
|
+
},
|
|
384
|
+
layer: inferLayerFromPath(f.path)
|
|
385
|
+
}));
|
|
386
|
+
return {
|
|
387
|
+
nodes,
|
|
388
|
+
edges: [],
|
|
389
|
+
source: this.source,
|
|
390
|
+
timestamp: Date.now()
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
extractFileList(files) {
|
|
394
|
+
if (Array.isArray(files)) return files.flatMap((f) => {
|
|
395
|
+
if (typeof f === "string") return [{ path: f }];
|
|
396
|
+
const rec = f;
|
|
397
|
+
if (typeof rec.path === "string") return [{
|
|
398
|
+
path: rec.path,
|
|
399
|
+
language: rec.language
|
|
400
|
+
}];
|
|
401
|
+
return [];
|
|
402
|
+
});
|
|
403
|
+
const obj = files;
|
|
404
|
+
const inner = obj?.files ?? obj?.items;
|
|
405
|
+
return Array.isArray(inner) ? this.extractFileList(inner) : [];
|
|
406
|
+
}
|
|
407
|
+
toAdapterResult(dbNodes, dbEdges) {
|
|
408
|
+
const nodes = mapRowsToGraphNodes(dbNodes);
|
|
409
|
+
const validNodeIds = new Set(nodes.map((n) => n.id));
|
|
410
|
+
const edges = mapRowsToGraphEdges(dbEdges, validNodeIds);
|
|
411
|
+
return {
|
|
412
|
+
nodes,
|
|
413
|
+
edges,
|
|
414
|
+
source: this.source,
|
|
415
|
+
timestamp: Date.now()
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
//#endregion
|
|
421
|
+
export { CODEGRAPH_SOURCE, CodeGraphAdapter, readEdgesForNodesFromDb, readEdgesFromDb, readFilesFromDb, readIncidentEdgesWithFilePathsFromDb, resolveDbPath, sortEdgesByKindGroup };
|
|
422
|
+
//# sourceMappingURL=CodeGraphAdapter-eyrhs0K0.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeGraphAdapter-eyrhs0K0.js","names":["EDGE_KIND_GROUP_ORDER: readonly string[]","edges: readonly T[]","e: T","dbPath: string","nodeId: string","scope: 'file' | 'symbol'","cursor: number","limit: number","e: DbEdgeRow","allEdges","total","pagedEdges","nodeIds: string[]","filePaths: string[]","workspacePath: string","CODEGRAPH_SOURCE: DataSourceType","repoId: string","invoke: UpstreamInvoker","cursor: GraphPageCursor | null","allNodes: DbNodeRow[]","allEdges: DbEdgeRow[]","nodes: GraphNode[]","files: unknown","dbNodes: DbNodeRow[]","dbEdges: DbEdgeRow[]"],"sources":["../src/adapters/codeGraphEdgeReads.ts","../src/adapters/CodeGraphAdapter.ts"],"sourcesContent":["/**\r\n * codegraph.db 的**边侧**读取(cycle-110 Q7 行为等价提取)。\r\n *\r\n * 抽取契约(每条都对应一条历史缺陷,改动前逐条确认):\r\n * · 分批 IN 查询只用于规避 SQLite 参数上限(`SQLITE.MAX_IN_PARAMS`);**绝不在分批内使用\r\n * LIMIT/OFFSET**——OFFSET 作用于整个结果集而非单个批次(CLAUDE.md 记录的既有教训)。\r\n * 因此凡是需要分页的路径,都是「先读全集、再在内存里 slice」。\r\n * · 过滤顺序:先过滤(自环 + `FILTERED_EDGE_KINDS`)**再**切片。`total` 是过滤后的计数,\r\n * 调用方按返回页长度推进游标;先切后滤会让 `total` 永远追不上、每页重复读同一批行。\r\n * · `readIncidentEdgesWithFilePathsFromDb` 刻意**不接受** cursor/limit:文件对聚合必须在分页\r\n * 之前完成(否则同一个文件对会被拆到多页里,每页各自聚合看起来都对、合并结果是错的)。\r\n * 它同时会把跨文件边的远端补查进 `filePathByNodeId`——否则远端会全部塌进 `'?'` 这个假文件对。\r\n * · 返回 `null` = 索引读不到(调用方回落到全量取数);返回空数组 = 读到了但没有数据。\r\n * 这两者绝不能合并(同 `readFilesFromDb` 的 `null` 语义)。\r\n */\r\nimport { scoped } from '../shared/Logger.ts';\r\nimport { SQLITE, INCREMENTAL } from '../shared/constants.ts';\r\nimport { EDGE_ROW_COLUMNS, FILTERED_EDGE_KINDS, mapEdgeKind, normalizeRepoPath, type DbNodeRow, type DbEdgeRow } from './codeGraphRows.ts';\r\nimport { getDbConnection, validateNodeRows, validateEdgeRows } from './codeGraphDb.ts';\nimport { collapseRepeatedBackslashes } from './nativeIndex.ts';\r\n\r\nconst log = scoped('codegraph-adapter');\r\n\r\n/**\r\n * file 域边列表的 kind 分组排序(cycle-139 / 需求分析 §82)。\r\n *\r\n * ## 它解决什么\r\n *\r\n * `/edges` 的 file 域返回「某文件的全部邻接边」。SQLite 无 ORDER BY(id 序返回),\r\n * 客户端拿到的边顺序是**索引插入序**——call/import/contains 混排,用户在边列表里\r\n * 找「谁调用了这个文件」要在几百条杂序边里翻。分组排序后同 kind 的边相邻,\r\n * 边列表呈现为可扫读的段(call 段 → import 段 → …)。\r\n *\r\n * ## 契约\r\n *\r\n * - **组序**:call → import → extend → implement → contains → dependency,\r\n * 未知/其他 kind 按 `mapEdgeKind` 归并后的字母序殿后(新 kind 不需要改本表,\r\n * 但会排到已知段之后——确定、可预期)。\r\n * - **组内序**:`(source, target, id)` 字典序——纯确定序,与 DB 返回序无关。\r\n * - **排序在过滤之后、切片之前**:与「先过滤再切片」既有契约同位(total 是过滤后\r\n * 计数;排序改变的是序不是集合,不改变 total)。\r\n * - **两条数据路径共用**:DB-first(`readEdgesFromDb` file 域 / 聚合路径的未切片全集)\r\n * 与内存回退各自调用本函数,不各写一份(「同语义共享一份实现」纪律)。\r\n * 组键兼容 `DbEdgeRow.kind` 与 `GraphEdge.type` 两种来源(见函数注)。\r\n */\r\nconst EDGE_KIND_GROUP_ORDER: readonly string[] = ['call', 'import', 'extend', 'implement', 'contains', 'dependency'];\r\n\r\n/**\r\n * 组键取值:`DbEdgeRow.kind`(DB 侧原始 kind)**或** `GraphEdge.type`(内存回落侧\r\n * 已映射的图类型)。两条路径的字段名不同——只读其一会让另一条路径的全部边落进\r\n * dependency 组、排序静默退化为坐标序(本轮实测踩坑:测试当场抓住)。\r\n */\r\nexport function sortEdgesByKindGroup<T extends { source: string; target: string; id: string | number; kind?: string; type?: string }>(edges: readonly T[]): T[] {\r\n const groupOf = (e: T): string => mapEdgeKind(e.kind ?? e.type ?? 'dependency');\r\n const groupRank = new Map<string, number>();\r\n EDGE_KIND_GROUP_ORDER.forEach((kind, i) => groupRank.set(kind, i));\r\n return [...edges].sort((a, b) => {\r\n const ga = groupOf(a);\r\n const gb = groupOf(b);\r\n if (ga !== gb) {\r\n const ra = groupRank.get(ga);\r\n const rb = groupRank.get(gb);\r\n // 已知组按表序;未知组殿后并按字母序(两者都缺省时字典序兜底)。\r\n if (ra !== undefined || rb !== undefined) return (ra ?? EDGE_KIND_GROUP_ORDER.length) - (rb ?? EDGE_KIND_GROUP_ORDER.length);\r\n return ga < gb ? -1 : ga > gb ? 1 : 0;\r\n }\r\n const sa = String(a.source);\r\n const sb = String(b.source);\r\n if (sa !== sb) return sa < sb ? -1 : 1;\r\n const ta = String(a.target);\r\n const tb = String(b.target);\r\n if (ta !== tb) return ta < tb ? -1 : 1;\r\n const ia = String(a.id);\r\n const ib = String(b.id);\r\n return ia < ib ? -1 : ia > ib ? 1 : 0;\r\n });\r\n}\r\n\r\n/**\r\n * Paged read of a node's incident edges, already filtered by the shared edge\r\n * policy (`FILTERED_EDGE_KINDS` + self-loops).\r\n *\r\n * The filter MUST run before slicing: `total` is the filtered count and the\r\n * caller advances its cursor by the returned page length, so filtering after\r\n * the slice would make `total` unreachable and re-read rows on every page.\r\n * `CodeGraphAdapter.fetchData` applies the same policy to `dbEdges`.\r\n */\r\nexport function readEdgesFromDb(dbPath: string, nodeId: string, scope: 'file' | 'symbol', cursor: number, limit: number): { edges: DbEdgeRow[]; total: number } | null {\r\n const db = getDbConnection(dbPath);\r\n if (!db) return null;\r\n const keep = (e: DbEdgeRow): boolean => e.source !== e.target && !FILTERED_EDGE_KINDS.has(e.kind);\r\n try {\r\n if (scope === 'symbol') {\r\n // symbol 域保持 id 序(节点邻接边数量小,kind 混排可读性问题不突出;\r\n // 引入分组会改变既有消费方的翻页快照序——刻意不动,契约变化最小化)。\r\n const allEdges = validateEdgeRows(db.prepare(\r\n `SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source = ? OR target = ?`,\r\n ).all(nodeId, nodeId)).filter(keep);\r\n const total = allEdges.length;\r\n const pagedEdges = allEdges.slice(cursor, cursor + limit);\r\n return { edges: pagedEdges, total };\r\n }\r\n const fileResult = readIncidentEdgesWithFilePathsFromDb(dbPath, nodeId, 'file');\r\n if (!fileResult) return null;\r\n // cycle-139:file 域全集先分组排序再切片——同 kind 相邻,且翻页序稳定\r\n //(与 DB 返回序解耦:索引重排/重扫后同一请求翻出的页序不变)。\r\n const allEdges = sortEdgesByKindGroup(fileResult.edges);\r\n const total = allEdges.length;\r\n const pagedEdges = allEdges.slice(cursor, cursor + limit);\r\n return { edges: pagedEdges, total };\r\n } catch (e) {\r\n log.warn('readEdgesFromDb failed', e);\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * 一次性读回某节点(或其所在文件)的全部邻接边,**不切片**,并附上每个端点所属文件。\r\n *\r\n * 供「文件对聚合」使用:聚合必须在分页之前完成(否则每页各自聚合,\r\n * 同一个文件对会被拆到多页里),所以聚合路径需要未切片的全集。\r\n * 调用方拿到结果后自行决定聚合还是切片,因此这里刻意不接受 cursor/limit。\r\n *\r\n * 遵守与 `readEdgesFromDb` 相同的约束:分批 IN 查询只用于规避 SQLite 参数上限,\r\n * 绝不在分批内使用 LIMIT/OFFSET(OFFSET 作用于整个结果集而非单个批次)。\r\n */\r\nexport function readIncidentEdgesWithFilePathsFromDb(\r\n dbPath: string,\r\n nodeId: string,\r\n scope: 'file' | 'symbol',\r\n): { edges: DbEdgeRow[]; filePathByNodeId: Map<string, string> } | null {\r\n const db = getDbConnection(dbPath);\r\n if (!db) return null;\r\n const keep = (e: DbEdgeRow): boolean => e.source !== e.target && !FILTERED_EDGE_KINDS.has(e.kind);\r\n try {\r\n const filePathByNodeId = new Map<string, string>();\r\n const edgeMap = new Map<number, DbEdgeRow>();\r\n const batchSize = Math.floor(SQLITE.MAX_IN_PARAMS / 2);\r\n\r\n if (scope === 'file') {\r\n const filePathResult = db.prepare('SELECT file_path FROM nodes WHERE id = ?').get(nodeId) as { file_path: string } | undefined;\r\n if (!filePathResult) return { edges: [], filePathByNodeId };\r\n const filePath = filePathResult.file_path;\r\n const nodeIds = db.prepare('SELECT id FROM nodes WHERE file_path = ?').all(filePath) as { id: string }[];\r\n const idList = nodeIds.map((n) => n.id);\r\n if (idList.length === 0) return { edges: [], filePathByNodeId };\r\n for (const id of idList) filePathByNodeId.set(id, filePath);\r\n for (let i = 0; i < idList.length; i += batchSize) {\r\n const batch = idList.slice(i, i + batchSize);\r\n const placeholders = batch.map(() => '?').join(', ');\r\n const batchEdges = validateEdgeRows(db.prepare(\r\n `SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source IN (${placeholders}) OR target IN (${placeholders})`,\r\n ).all(...batch, ...batch));\r\n for (const e of batchEdges) edgeMap.set(e.id, e);\r\n }\r\n } else {\r\n const symbolEdges = validateEdgeRows(db.prepare(\r\n `SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source = ? OR target = ?`,\r\n ).all(nodeId, nodeId));\r\n for (const e of symbolEdges) edgeMap.set(e.id, e);\r\n }\r\n\r\n const edges = [...edgeMap.values()].filter(keep);\r\n\r\n // 解析尚未知文件的端点(跨文件边的远端)。文件作用域下只有本文件内的节点\r\n // 预先有映射,远端必须回查,否则它们会被归到 '?' 这一个假文件对里。\r\n const unresolved = new Set<string>();\r\n for (const e of edges) {\r\n if (!filePathByNodeId.has(e.source)) unresolved.add(e.source);\r\n if (!filePathByNodeId.has(e.target)) unresolved.add(e.target);\r\n }\r\n const unresolvedList = [...unresolved];\r\n for (let i = 0; i < unresolvedList.length; i += batchSize) {\r\n const batch = unresolvedList.slice(i, i + batchSize);\r\n const placeholders = batch.map(() => '?').join(',');\r\n const rows = db.prepare(\r\n `SELECT id, file_path FROM nodes WHERE id IN (${placeholders})`,\r\n ).all(...batch) as { id: string; file_path: string }[];\r\n for (const r of rows) filePathByNodeId.set(r.id, r.file_path);\r\n }\r\n\r\n return { edges, filePathByNodeId };\r\n } catch (e) {\r\n log.warn('readIncidentEdgesWithFilePathsFromDb failed', e);\r\n return null;\r\n }\r\n}\r\n\r\nexport function readEdgesForNodesFromDb(dbPath: string, nodeIds: string[]): DbEdgeRow[] {\r\n if (nodeIds.length === 0) return [];\r\n const db = getDbConnection(dbPath);\r\n if (!db) return [];\r\n try {\r\n const edgeMap = new Map<number, DbEdgeRow>();\r\n // 存量脏库兼容(cycle-141 第六轮):修复前写入的边端点带**双反斜杠**,调用方传\r\n // 的是折叠后的单形态 id——查询集合同时包含两种拼写(Set 天然去重),双形态\r\n // 库与消毒后的新库都能命中。返回行的端点原样返回;bootstrap 的\r\n // mapDbEdgesToGraphEdges 端点过滤用同一个双形态集合,闭环成立。\r\n const nodeSet = new Set<string>();\r\n for (const id of nodeIds) {\r\n nodeSet.add(id);\r\n // 脏库方向是「扩展」:单形态 → 双形态(脏库每个分隔符都是两个字符)。\r\n const expanded = id.replace(/\\\\/g, '\\\\\\\\');\r\n if (expanded !== id) nodeSet.add(expanded);\r\n }\r\n const batchSize = Math.floor(SQLITE.MAX_IN_PARAMS / 2);\r\n for (let i = 0; i < nodeIds.length; i += batchSize) {\r\n const batch = nodeIds.slice(i, i + batchSize);\r\n const queryBatch = batch.flatMap((id) => {\r\n const expanded = id.replace(/\\\\/g, '\\\\\\\\');\r\n return expanded !== id ? [id, expanded] : [id];\r\n });\r\n const placeholders = queryBatch.map(() => '?').join(', ');\r\n const edges = validateEdgeRows(db.prepare(\r\n `SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source IN (${placeholders}) OR target IN (${placeholders})`,\r\n ).all(...queryBatch, ...queryBatch));\r\n for (const e of edges) {\r\n if (nodeSet.has(e.source) && nodeSet.has(e.target)) edgeMap.set(e.id, e);\r\n }\r\n }\r\n // 端点折叠(与节点行的 sanitizeDirtyRows 同一出口纪律):查询集合允许双形态\r\n // 是为了命中脏库,但**返回行必须与调用方的 id 域一致**——调用方传的是折叠后的\r\n // 单形态(节点行已消毒),边端点若原样返回(双形态),同一次响应里边端点与\r\n // 节点 id 不同域,客户端聚类聚合的 nodeIdToVisibleId 映射不上,边全丢\r\n //(真机复现:bootstrap 边 41 条全被聚合丢弃,画布 0 边)。\r\n const result = [...edgeMap.values()];\r\n for (const e of result) {\r\n (e as { source: string }).source = collapseRepeatedBackslashes(e.source);\r\n (e as { target: string }).target = collapseRepeatedBackslashes(e.target);\r\n }\r\n return result;\r\n } catch (e) {\r\n log.warn('readEdgesForNodesFromDb failed', e);\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * Read the nodes of a set of changed files plus every edge that touches one of them (P2-21).\r\n *\r\n * Both separator spellings of each path are tried, and a changed *directory* is matched by prefix so\r\n * that a file added under a new folder still pulls that folder's rows.\r\n *\r\n * Edges are returned when **either** endpoint is in the touched node set, not only when both are: an\r\n * edge from a changed file into an unchanged one is exactly the kind of edge a re-scan can add or\r\n * remove, and filtering it out here would make it invisible to the caller. The caller is responsible\r\n * for dropping edges whose far endpoint no longer exists in the resulting graph.\r\n *\r\n * Returns `null` when the index cannot be read at all, which callers treat as \"fall back to a full\r\n * fetch\"; an empty result means the index was read and holds nothing for those paths.\r\n */\r\nexport function readFilesFromDb(dbPath: string, filePaths: string[]): { nodes: DbNodeRow[]; edges: DbEdgeRow[] } | null {\r\n if (filePaths.length === 0) return { nodes: [], edges: [] };\r\n const db = getDbConnection(dbPath);\r\n if (!db) return null;\r\n\r\n try {\r\n const wanted = new Set<string>();\r\n for (const raw of filePaths) {\r\n const normalized = normalizeRepoPath(raw);\r\n if (!normalized) continue;\r\n wanted.add(normalized);\r\n wanted.add(normalized.replace(/\\//g, '\\\\'));\r\n }\r\n const candidates = [...wanted];\r\n if (candidates.length === 0) return { nodes: [], edges: [] };\r\n\r\n const batchSize = Math.min(INCREMENTAL.MAX_FILE_BATCH, Math.floor(SQLITE.MAX_IN_PARAMS / 2));\r\n const nodeMap = new Map<string, DbNodeRow>();\r\n for (let i = 0; i < candidates.length; i += batchSize) {\r\n const batch = candidates.slice(i, i + batchSize);\r\n const exact = batch.map(() => '?').join(',');\r\n const prefix = batch.map(() => \"file_path LIKE ? ESCAPE '\\\\'\").join(' OR ');\r\n const prefixArgs = batch.map((p) => `${p.replace(/[%_\\\\]/g, '\\\\$&')}/%`);\r\n const rows = validateNodeRows(db.prepare(\r\n `SELECT id, kind, name, file_path, start_line, signature, docstring, is_exported FROM nodes\r\n WHERE file_path IN (${exact}) OR ${prefix}`,\r\n ).all(...batch, ...prefixArgs));\r\n for (const n of rows) nodeMap.set(n.id, n);\r\n }\r\n\r\n if (nodeMap.size === 0) return { nodes: [], edges: [] };\r\n\r\n const nodeIds = [...nodeMap.keys()];\r\n const edgeMap = new Map<number, DbEdgeRow>();\r\n for (let i = 0; i < nodeIds.length; i += batchSize) {\r\n const batch = nodeIds.slice(i, i + batchSize);\r\n const placeholders = batch.map(() => '?').join(', ');\r\n const rows = validateEdgeRows(db.prepare(\r\n `SELECT ${EDGE_ROW_COLUMNS} FROM edges WHERE source IN (${placeholders}) OR target IN (${placeholders})`,\r\n ).all(...batch, ...batch));\r\n for (const e of rows) edgeMap.set(e.id, e);\r\n }\r\n\r\n return { nodes: [...nodeMap.values()], edges: [...edgeMap.values()] };\r\n } catch (e) {\r\n log.warn('readFilesFromDb failed', e);\r\n return null;\r\n }\r\n}\r\n","// CodeGraphAdapter - adapter for the optional dsh-codegraph data source.\n//\n// Primary path: read the .codegraph/codegraph.db SQLite index directly\n// (node:sqlite, Node >= 22.5). One O(N+E) query returns the whole graph —\n// no N+1 tool calls, works even when dsh-codegraph's tool surface is 'core'\n// (which exposes no full-graph query tool).\n//\n// Fallback path: upstream tools (codegraph_query / codegraph_callers /\n// codegraph_callees) when the DB cannot be read (e.g. remote sandboxed fs).\n//\n// cycle-110 Q7:本文件原先 795 行 / band-detector 161(预算 100、上带 115),是与\n// `api/index.ts`、`progressiveCluster.ts` 并列的三个超预算热点之一。已按职责拆成三层,\n// **只移动代码、不改任何语义**,并把全部原有导出名在这里原样 re-export,使 30+ 个\n// 用例与生产调用点的 import 路径零改动:\n// · `codeGraphRows.ts` —— 行 → 图节点/图边的唯一映射(含过滤与 import 截断)\n// · `codeGraphDb.ts` —— 连接池、行校验、节点侧读取、分页游标\n// · `codeGraphEdgeReads.ts` —— 边侧读取(邻接边、文件对全集、定向读)\n// 本文件只剩「adapter 本体 + 全量取数编排 + 上游工具回落」。\nimport { normalize } from 'node:path';\nimport { join } from 'node:path';\nimport type { GraphNode, AdapterResult, DataSourceType } from '../types/index.ts';\nimport { NodeId } from '../types/index.ts';\nimport { inferLayerFromPath } from '../shared/layerUtils.ts';\nimport type { IDataAdapter, UpstreamInvoker } from './IAdapter.ts';\nimport type { DbNodeRow, DbEdgeRow } from './codeGraphRows.ts';\nimport { isNoisePath, mapRowsToGraphNodes, mapRowsToGraphEdges } from './codeGraphRows.ts';\nimport { readGraphFromDbPaged } from './codeGraphDb.ts';\nimport type { GraphPageCursor } from './codeGraphDb.ts';\n\nexport type { UpstreamInvoker } from './IAdapter.ts';\n\n// —— 公开面保持:拆到三层的符号全部原样再导出(生产代码与用例的 import 路径零改动) ——\nexport type { DbNodeRow, DbEdgeRow } from './codeGraphRows.ts';\nexport {\n EDGE_ROW_COLUMNS,\n FILTERED_EDGE_KINDS,\n isNoisePath,\n mapRowsToGraphNodes,\n mapRowsToGraphEdges,\n mapNodeKind,\n mapEdgeKind,\n normalizeRepoPath,\n parseEdgeConfidence,\n} from './codeGraphRows.ts';\nexport {\n closeAllDbConnections,\n countEdgesInDb,\n readGraphFromDb,\n readGraphFromDbPaged,\n readTopLevelNodesFromDb,\n readNodeChildrenFromDb,\n readFileOutlineFromDb,\n readSymbolASTFromDb,\n} from './codeGraphDb.ts';\nexport type { GraphPageCursor, GraphPageResult } from './codeGraphDb.ts';\nexport {\n readEdgesFromDb,\n readIncidentEdgesWithFilePathsFromDb,\n readEdgesForNodesFromDb,\n readFilesFromDb,\n sortEdgesByKindGroup,\n} from './codeGraphEdgeReads.ts';\n\n/**\n * Locate the .codegraph/codegraph.db for a workspace path.\n * Callers MUST validate workspacePath via resolveWorkspaceFromRequest or\n * validateWorkspacePath before calling this function.\n */\nexport function resolveDbPath(workspacePath: string): string {\n const root = workspacePath && workspacePath !== '.' ? workspacePath : process.cwd();\n const normalized = normalize(root);\n if (normalized.includes('..')) {\n throw new Error(`Invalid workspace path: ${workspacePath}`);\n }\n return join(normalized, '.codegraph', 'codegraph.db');\n}\n\n/** The `source` every row this adapter produces is attributed to. */\nexport const CODEGRAPH_SOURCE: DataSourceType = 'codegraph';\n\nexport class CodeGraphAdapter implements IDataAdapter {\n readonly source: DataSourceType = CODEGRAPH_SOURCE;\n\n async fetchData(repoId: string, invoke: UpstreamInvoker): Promise<AdapterResult> {\n // Primary: paged SQLite read of the upstream index (avoids OOM on large repos).\n const dbPath = resolveDbPath(repoId);\n let cursor: GraphPageCursor | null = null;\n const allNodes: DbNodeRow[] = [];\n const allEdges: DbEdgeRow[] = [];\n const PAGE = 500;\n do {\n const page = readGraphFromDbPaged(dbPath, cursor, PAGE);\n if (!page) { allNodes.length = 0; break; }\n allNodes.push(...page.nodes);\n allEdges.push(...page.edges);\n cursor = page.nextCursor;\n } while (cursor !== null);\n\n if (allNodes.length > 0) {\n return this.toAdapterResult(allNodes, allEdges);\n }\n\n // Fallback (DB unreadable): assemble a file-structure skeleton from the\n // upstream `codegraph_files` tool so the panel at least shows the project\n // layout. No relations — the full graph needs the DB.\n const files = await invoke('codegraph_files', { path: repoId, format: 'flat' });\n if (!files || typeof files !== 'object') {\n return { nodes: [], edges: [], source: this.source, timestamp: Date.now() };\n }\n const rawFiles = this.extractFileList(files);\n const nodes: GraphNode[] = rawFiles\n .filter((f) => !isNoisePath(f.path))\n .map((f, i) => ({\n id: NodeId(`file:${f.path}`),\n label: f.path.split('/').pop() ?? f.path,\n type: 'module',\n filePath: f.path,\n lineNumber: 1,\n properties: { kind: 'file', language: f.language ?? null, index: i },\n layer: inferLayerFromPath(f.path),\n }));\n return { nodes, edges: [], source: this.source, timestamp: Date.now() };\n }\n\n private extractFileList(files: unknown): Array<{ path: string; language?: string }> {\n // codegraph_files --json returns either an array of file records or an\n // object with a files/files[] key, depending on CLI version.\n if (Array.isArray(files)) {\n return files.flatMap((f) => {\n if (typeof f === 'string') return [{ path: f }];\n const rec = f as Record<string, unknown>;\n if (typeof rec.path === 'string') return [{ path: rec.path, language: rec.language as string | undefined }];\n return [];\n });\n }\n const obj = files as Record<string, unknown>;\n const inner = obj?.files ?? obj?.items;\n return Array.isArray(inner) ? this.extractFileList(inner) : [];\n }\n\n private toAdapterResult(dbNodes: DbNodeRow[], dbEdges: DbEdgeRow[]): AdapterResult {\n // 节点/边的映射与过滤全部委托给共享实现,增量路径走同一份逻辑(见 mapRowsToGraphNodes /\n // mapRowsToGraphEdges)。这里不传 `source`:`__source` 由 GraphDataMerger.merge 统一打标。\n const nodes = mapRowsToGraphNodes(dbNodes);\n const validNodeIds = new Set<string>(nodes.map((n) => n.id as string));\n const edges = mapRowsToGraphEdges(dbEdges, validNodeIds);\n return { nodes, edges, source: this.source, timestamp: Date.now() };\n }\n}\n"],"mappings":"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"}
|