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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
//#region src/adapters/treeSitterExtractors.ts
|
|
2
|
+
/**
|
|
3
|
+
* 语法事实提取器集合(模块级纯函数,复杂度独立记账)。
|
|
4
|
+
*
|
|
5
|
+
* 归属理由:`TreeSitterAdapter.walk` 与 `FileSystemAdapter.fetchDataWithSymbols`
|
|
6
|
+
* 的复杂度都已逼近预算上限(band-detector 口径),因此凡是「读一棵语法树 / 读一张
|
|
7
|
+
* 表给出决策」的逻辑都放在本文件——它们不碰 fs、不碰 logger、可被单独测试。
|
|
8
|
+
*
|
|
9
|
+
* 本文件当前承载四组能力:
|
|
10
|
+
* 1. import 模块名提取(cycle-129 · T1)——{@link IMPORT_STATEMENT_TYPES} /
|
|
11
|
+
* {@link extractImportSources};
|
|
12
|
+
* 2. 继承目标提取(cycle-123)——{@link collectHeritageTargets};
|
|
13
|
+
* 3. tsconfig paths 别名解析(cycle-130 · T1)——{@link parseTsconfigPaths} /
|
|
14
|
+
* {@link resolveAliasModule};
|
|
15
|
+
* 4. **调用点提取与 call 边解析(T2)**——{@link CALL_EXPRESSION_TYPES} /
|
|
16
|
+
* {@link extractCalleeName} / {@link maybeCollectCallSite} /
|
|
17
|
+
* {@link resolveCallEdges}。修前 `src/adapters` 全目录 `call_expression` 命中数为 0,
|
|
18
|
+
* 原生通道产出的 `call` 边恒为 0;而 `DEFAULT_PATH_EDGE_TYPES`
|
|
19
|
+
* (`src/shared/analysis/XuanjiAdapter.ts`)含 `'call'`——于是上游 dsh-codegraph
|
|
20
|
+
* 缺席时,影响面 / 调用链 / 环检测全部读到空图。
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* import 语句节点类型(cycle-129 · T1):四类覆盖 TS/JS/Python/Rust/Java 的
|
|
24
|
+
* import 语法形态。walk 拦截这些节点后调 {@link extractImportSources}。
|
|
25
|
+
*/
|
|
26
|
+
const IMPORT_STATEMENT_TYPES = new Set([
|
|
27
|
+
"import_statement",
|
|
28
|
+
"import_declaration",
|
|
29
|
+
"import_from_statement",
|
|
30
|
+
"use_declaration"
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* 从一个 import 语法节点提取模块名(模块级纯函数,复杂度独立记账)。
|
|
34
|
+
* 语法形态与降级规则见 walk 内注释(TreeSitterAdapter.ts cycle-129 T1 段)。
|
|
35
|
+
*/
|
|
36
|
+
function extractImportSources(node) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const push = (raw) => {
|
|
39
|
+
if (!raw) return;
|
|
40
|
+
const cleaned = raw.replace(/^['"`]|['"`]$/g, "").trim();
|
|
41
|
+
if (cleaned.length > 1) out.push(cleaned);
|
|
42
|
+
};
|
|
43
|
+
const sourceNode = node.childForFieldName?.("source");
|
|
44
|
+
if (sourceNode) {
|
|
45
|
+
push(sourceNode.text);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
const moduleName = node.childForFieldName?.("module_name") ?? node.childForFieldName?.("name") ?? node.childForFieldName?.("argument");
|
|
49
|
+
if (moduleName) {
|
|
50
|
+
const text = moduleName.text.replace(/^\.+/, "").trim();
|
|
51
|
+
if (text.length > 1) out.push(text);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function collectHeritageTargets(declaration) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const childByType = (t) => declaration.children?.find((c) => c.type === t);
|
|
58
|
+
const extendsClause = childByType("extends_type_clause");
|
|
59
|
+
if (extendsClause) {
|
|
60
|
+
for (const c of extendsClause.children ?? []) if (c.type === "type_identifier" && c.text) out.push({
|
|
61
|
+
clauseKind: "extends",
|
|
62
|
+
targetName: c.text
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const heritage = childByType("class_heritage");
|
|
66
|
+
if (heritage) for (const clause of heritage.children ?? []) {
|
|
67
|
+
const kind = clause.type === "extends_clause" ? "extends" : clause.type === "implements_clause" ? "implements" : null;
|
|
68
|
+
if (!kind) continue;
|
|
69
|
+
for (const c of clause.children ?? []) if ((c.type === "type_identifier" || c.type === "identifier") && c.text) out.push({
|
|
70
|
+
clauseKind: kind,
|
|
71
|
+
targetName: c.text
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const superclasses = declaration.childForFieldName?.("superclasses");
|
|
75
|
+
if (superclasses) {
|
|
76
|
+
for (const c of superclasses.children ?? []) if ((c.type === "identifier" || c.type === "attribute") && c.text) out.push({
|
|
77
|
+
clauseKind: "extends",
|
|
78
|
+
targetName: c.text
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 调用表达式节点类型集合(跨 grammar 的并集)。
|
|
85
|
+
* walk 拦截这些节点后交给 {@link maybeCollectCallSite} / {@link extractCalleeName}。
|
|
86
|
+
*/
|
|
87
|
+
const CALL_EXPRESSION_TYPES = new Set([
|
|
88
|
+
"call_expression",
|
|
89
|
+
"call",
|
|
90
|
+
"method_invocation"
|
|
91
|
+
]);
|
|
92
|
+
/**
|
|
93
|
+
* 名字节点的类型集合:这些节点的 `text` 就是末段标识符本身。
|
|
94
|
+
* go / rust 的 `field_identifier`、TS 的 `private_property_identifier`
|
|
95
|
+
* (`this.#p()` 实测)都在此列。
|
|
96
|
+
*/
|
|
97
|
+
const NAME_NODE_TYPES = new Set([
|
|
98
|
+
"identifier",
|
|
99
|
+
"property_identifier",
|
|
100
|
+
"field_identifier",
|
|
101
|
+
"private_property_identifier"
|
|
102
|
+
]);
|
|
103
|
+
/**
|
|
104
|
+
* 末段字段表:节点类型 → 直接保存末段名字的字段名。
|
|
105
|
+
*
|
|
106
|
+
* 用查表替代 switch 是有意的(复杂度预算):12 个 case 的分支数等于 12 个 if,
|
|
107
|
+
* 而这里的三张表各占一行。六种 grammar 实测的字段归属如下——
|
|
108
|
+
* member_expression 的末段在 `property`,python attribute 在 `attribute`,
|
|
109
|
+
* rust scoped_identifier 在 `name`,rust field_expression 与 go selector_expression
|
|
110
|
+
* 都在 `field`。
|
|
111
|
+
*/
|
|
112
|
+
const TAIL_FIELD_BY_TYPE = Object.freeze({
|
|
113
|
+
member_expression: "property",
|
|
114
|
+
attribute: "attribute",
|
|
115
|
+
scoped_identifier: "name",
|
|
116
|
+
field_expression: "field",
|
|
117
|
+
selector_expression: "field"
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* 包装节点表:节点类型 → 内层表达式所在的字段名。
|
|
121
|
+
* rust 的实例化调用 `o.m::<i32>()` 外面套 `generic_function`(内层在 `function`),
|
|
122
|
+
* go 的 `foo[int]()` / `obj.M[int]()` 套 `index_expression`(内层在 `operand`,
|
|
123
|
+
* `index` 是类型实参而非函数表达式)。
|
|
124
|
+
*/
|
|
125
|
+
const INNER_FIELD_BY_TYPE = Object.freeze({
|
|
126
|
+
generic_function: "function",
|
|
127
|
+
index_expression: "operand"
|
|
128
|
+
});
|
|
129
|
+
/** 取节点末段的**名字子节点**(不是文本切分——文本切分会被泛型/可选链带偏)。 */
|
|
130
|
+
function lastNamedChild(node) {
|
|
131
|
+
return [...node.children ?? []].reverse().find((c) => NAME_NODE_TYPES.has(c.type)) ?? null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 从「被调方表达式」节点取简单名。六种 grammar 实测形态:
|
|
135
|
+
*
|
|
136
|
+
* - 裸标识符(TS/Py/Rust/Go 的直接调用)→ 自身 text;
|
|
137
|
+
* - 末段字段表命中的包装节点 → 该字段;字段缺席时退回最后一个名字子节点
|
|
138
|
+
* (可选链 `a?.b` 的子节点是 `identifier / optional_chain / property_identifier`,
|
|
139
|
+
* 字段仍然可读,但退回路径覆盖语法恢复产出的异常形态);
|
|
140
|
+
* - 包装节点表命中的 → 递归内层表达式;
|
|
141
|
+
* - 括号表达式 `(f)()` → 递归唯一非括号子节点;
|
|
142
|
+
* - TS `arr[0]()` 的 `subscript_expression` **刻意不取名**:下标不是符号引用,
|
|
143
|
+
* 硬取会把 `number` 的文本当符号名(宁缺勿造)。
|
|
144
|
+
*
|
|
145
|
+
* java 的 `method_invocation` 没有被调方字段,直接读 `name`
|
|
146
|
+
* (`obj.<T>g()` 实测 `name=g`,泛型参数不影响)。
|
|
147
|
+
*/
|
|
148
|
+
function calleeFromExpression(node, depth) {
|
|
149
|
+
if (depth > 6) return null;
|
|
150
|
+
if (NAME_NODE_TYPES.has(node.type)) return node.text || null;
|
|
151
|
+
const tailField = TAIL_FIELD_BY_TYPE[node.type];
|
|
152
|
+
if (tailField) return node.childForFieldName?.(tailField)?.text || lastNamedChild(node)?.text || null;
|
|
153
|
+
const innerField = INNER_FIELD_BY_TYPE[node.type];
|
|
154
|
+
const inner = innerField ? node.childForFieldName?.(innerField) : node.type === "parenthesized_expression" ? (node.children ?? []).find((c) => c.type !== "(" && c.type !== ")") : null;
|
|
155
|
+
return inner ? calleeFromExpression(inner, depth + 1) : null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 从调用语法节点提取被调方的**简单名**(末段标识符);无名字返回 null。
|
|
159
|
+
*
|
|
160
|
+
* 「简单名」是刻意的上限:本通道没有类型信息,`a.b()` 只能知道「有个叫 b 的调用」,
|
|
161
|
+
* 不能知道 `a` 到底是什么类型。因此下游解析必须按「同名符号是否唯一」裁决,
|
|
162
|
+
* 多义时**跳过而不是猜**(见 {@link resolveCallEdges})。
|
|
163
|
+
*/
|
|
164
|
+
function extractCalleeName(node) {
|
|
165
|
+
const nameField = node.childForFieldName?.("name");
|
|
166
|
+
if (node.type === "method_invocation") return nameField?.text || null;
|
|
167
|
+
const callee = node.childForFieldName?.("function");
|
|
168
|
+
if (!callee) return null;
|
|
169
|
+
return calleeFromExpression(callee, 0);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 收集一个调用点(若它的宿主符号合法)。
|
|
173
|
+
*
|
|
174
|
+
* ## 为什么必须要求宿主是可调用符号
|
|
175
|
+
*
|
|
176
|
+
* `walk` 用栈遍历,压栈时携带的 `parentId` 是**最近的一个符号节点**——它可能是
|
|
177
|
+
* class、variable,也可能是 module(`import_statement` 本身就是一个 module 符号)。
|
|
178
|
+
* 模块顶层的 `foo()` 若记成 `file → foo` 的 call 边,等于宣称「这个文件调用了 foo」;
|
|
179
|
+
* `const x = foo()` 若记成 `x → foo`,等于宣称「变量 x 调用了 foo」。这两类边都会
|
|
180
|
+
* 让调用链读法出现不存在的路径,所以:**宿主不是 function 类型就直接跳过**(宁缺勿造)。
|
|
181
|
+
* 代价是模块级初始化调用(`const db = openDb()`)不产生 call 边——这是刻意接受的缺口。
|
|
182
|
+
*
|
|
183
|
+
* 本函数同时承担「调用节点 → 调用点」的判定,walk 里只用一行调用 + 一行 push,
|
|
184
|
+
* 不再新增条件语句(复杂度预算:`walk` 所在文件已贴近 100)。
|
|
185
|
+
*
|
|
186
|
+
* @returns 新的调用点,或 null(不是调用节点 / 宿主非法 / 无名调用)。
|
|
187
|
+
*/
|
|
188
|
+
function maybeCollectCallSite(node, nodeType, hostId, hostType) {
|
|
189
|
+
if (!CALL_EXPRESSION_TYPES.has(nodeType)) return null;
|
|
190
|
+
if (!hostId || hostType !== "function") return null;
|
|
191
|
+
const calleeName = extractCalleeName(node);
|
|
192
|
+
if (!calleeName) return null;
|
|
193
|
+
return {
|
|
194
|
+
callerId: hostId,
|
|
195
|
+
calleeName,
|
|
196
|
+
line: node.startPosition.row + 1
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** TODO/FIXME 两种标记的字面量与写入字段(一张表替代四段重复分支)。 */
|
|
200
|
+
const TODO_MARKERS = [[
|
|
201
|
+
"TODO",
|
|
202
|
+
"todo",
|
|
203
|
+
"todoCount"
|
|
204
|
+
], [
|
|
205
|
+
"FIXME",
|
|
206
|
+
"fixme",
|
|
207
|
+
"fixmeCount"
|
|
208
|
+
]];
|
|
209
|
+
/**
|
|
210
|
+
* 注释里的 TODO/FIXME 标记 → 打到宿主符号节点上(cycle-122 的 todoMarkerMode 数据面)。
|
|
211
|
+
*
|
|
212
|
+
* 从 `TreeSitterAdapter.walk` 里搬出来的原因纯粹是**复杂度预算**:那一块原有 5 个条件
|
|
213
|
+
* 语句,而 walk 本身已到 101。搬动时**必须逐行等价**,否则「重构」会悄悄改掉行为——
|
|
214
|
+
* 这里特意保留原实现的两处容易改错的语义:
|
|
215
|
+
*
|
|
216
|
+
* 1. **计数是累加**(同一宿主下多条注释各 +1),标签只加一次(已有就不重复加);
|
|
217
|
+
* 2. `hasTodoFixme` 的判据是「命中了**任意**标记」,包括没有专属标签的 HACK / XXX / BUG
|
|
218
|
+
* ——所以它写在循环**外面**。挪进循环体会让「只有 HACK 的注释」不再置位,
|
|
219
|
+
* 而 `TODO_PATTERNS` 里 HACK/XXX/BUG 确实是合法命中项。
|
|
220
|
+
*
|
|
221
|
+
* 无标记命中时不碰 `tags`(保持 undefined,而不是写一个空数组):原实现只在需要
|
|
222
|
+
* 追加时才赋值,`tags: []` 与 `tags: undefined` 在节点上是两种可观测状态。
|
|
223
|
+
*/
|
|
224
|
+
function applyTodoMarkers(parentNode, matches) {
|
|
225
|
+
const tags = parentNode.tags ?? [];
|
|
226
|
+
for (const [marker, tag, countKey] of TODO_MARKERS) {
|
|
227
|
+
if (!matches.includes(marker)) continue;
|
|
228
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
229
|
+
parentNode.properties[countKey] = (parentNode.properties[countKey] ?? 0) + 1;
|
|
230
|
+
}
|
|
231
|
+
if (tags.length > 0) parentNode.tags = tags;
|
|
232
|
+
parentNode.properties.hasTodoFixme = true;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 符号节点的名字(label)提取:`name` 字段 → `type` 字段(Rust `impl_item` 没有
|
|
236
|
+
* `name`,它的 `type` 字段是被实现的类型,如 `Bar`)→ 第一个带名字的子节点。
|
|
237
|
+
*
|
|
238
|
+
* 六种 grammar 实测:符号节点的标签子节点总是 `identifier` / `property_identifier` /
|
|
239
|
+
* `type_identifier` 之一。三者都没有的节点保留显式 `<node.type>` 标记——
|
|
240
|
+
* `arrow_function` / `closure_expression` 没有名字,本来就该这样呈现。
|
|
241
|
+
*/
|
|
242
|
+
function extractSymbolName(node) {
|
|
243
|
+
const nameNode = node.childForFieldName?.("name") || node.childForFieldName?.("type") || node.children?.find((c) => c.type === "identifier" || c.type === "property_identifier" || c.type === "type_identifier");
|
|
244
|
+
return nameNode ? nameNode.text : `<${node.type}>`;
|
|
245
|
+
}
|
|
246
|
+
/** 按 label 建索引(同名多值保留全部,供唯一性判定)。 */
|
|
247
|
+
function indexByLabel(symbols) {
|
|
248
|
+
const map = new Map();
|
|
249
|
+
for (const symbol of symbols) map.set(symbol.label, [...map.get(symbol.label) ?? [], symbol.id]);
|
|
250
|
+
return map;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 把一批调用点解析成 `call` 边(含证据分级)。
|
|
254
|
+
*
|
|
255
|
+
* 边 id 形如 `tscall:${callerId}:${calleeId}`——与既有 `tsimport:` / `tsedge:`
|
|
256
|
+
* 前缀风格一致。同一对 (caller, callee) 出现多次调用只产出一条边
|
|
257
|
+
* (调用次数不是图结构;重复边会被 merger 按 id 去重,但这里先去重可以省掉
|
|
258
|
+
* 一次属性并集,也避免 ambiguous/unresolved 统计被重复调用放大)。
|
|
259
|
+
*/
|
|
260
|
+
function resolveCallEdges(input) {
|
|
261
|
+
const edges = [];
|
|
262
|
+
const seenEdgeIds = new Set();
|
|
263
|
+
let ambiguous = 0;
|
|
264
|
+
let unresolved = 0;
|
|
265
|
+
let exactCount = 0;
|
|
266
|
+
let inferredCount = 0;
|
|
267
|
+
const localIndex = indexByLabel(input.localSymbols);
|
|
268
|
+
const importedIndexes = [];
|
|
269
|
+
for (const symbols of input.importedFileSymbols.values()) importedIndexes.push(indexByLabel(symbols));
|
|
270
|
+
for (const site of input.callSites) {
|
|
271
|
+
const localHits = localIndex.get(site.calleeName) ?? [];
|
|
272
|
+
let calleeId = null;
|
|
273
|
+
let evidence = null;
|
|
274
|
+
if (localHits.length > 1) {
|
|
275
|
+
ambiguous++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (localHits.length === 1) {
|
|
279
|
+
calleeId = localHits[0] ?? null;
|
|
280
|
+
evidence = "exact";
|
|
281
|
+
} else {
|
|
282
|
+
let crossHit = null;
|
|
283
|
+
let crossAmbiguous = false;
|
|
284
|
+
for (const index of importedIndexes) {
|
|
285
|
+
const hits = index.get(site.calleeName) ?? [];
|
|
286
|
+
if (hits.length > 1) {
|
|
287
|
+
crossAmbiguous = true;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (hits.length === 1) {
|
|
291
|
+
if (crossHit !== null) {
|
|
292
|
+
crossAmbiguous = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
crossHit = hits[0] ?? null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (crossAmbiguous) {
|
|
299
|
+
ambiguous++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (crossHit === null) {
|
|
303
|
+
unresolved++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
calleeId = crossHit;
|
|
307
|
+
evidence = "inferred";
|
|
308
|
+
}
|
|
309
|
+
if (!calleeId) {
|
|
310
|
+
unresolved++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (calleeId === String(site.callerId)) continue;
|
|
314
|
+
const edgeId = `tscall:${String(site.callerId)}:${calleeId}`;
|
|
315
|
+
if (seenEdgeIds.has(edgeId)) continue;
|
|
316
|
+
seenEdgeIds.add(edgeId);
|
|
317
|
+
const grade = evidence ?? "inferred";
|
|
318
|
+
if (grade === "exact") exactCount++;
|
|
319
|
+
else inferredCount++;
|
|
320
|
+
edges.push({
|
|
321
|
+
id: edgeId,
|
|
322
|
+
source: String(site.callerId),
|
|
323
|
+
target: calleeId,
|
|
324
|
+
properties: {
|
|
325
|
+
edgeOrigin: grade === "exact" ? "ast" : "heuristic",
|
|
326
|
+
evidence: grade,
|
|
327
|
+
calleeName: site.calleeName,
|
|
328
|
+
line: site.line
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
edges,
|
|
334
|
+
ambiguous,
|
|
335
|
+
unresolved,
|
|
336
|
+
byEvidence: {
|
|
337
|
+
exact: exactCount,
|
|
338
|
+
inferred: inferredCount
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/** 从 tsconfig JSON 文本提取 paths 映射(宽松解析:JSONC 注释/尾逗号容错)。 */
|
|
343
|
+
function parseTsconfigPaths(jsonText) {
|
|
344
|
+
const cleaned = jsonText.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/[^\n]*/g, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
345
|
+
let parsed;
|
|
346
|
+
try {
|
|
347
|
+
parsed = JSON.parse(cleaned);
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const compilerOptions = parsed.compilerOptions;
|
|
352
|
+
if (!compilerOptions || typeof compilerOptions !== "object") return null;
|
|
353
|
+
const paths = compilerOptions.paths;
|
|
354
|
+
if (!paths || typeof paths !== "object") return null;
|
|
355
|
+
const baseUrlRaw = typeof compilerOptions.baseUrl === "string" ? compilerOptions.baseUrl : ".";
|
|
356
|
+
const baseUrl = baseUrlRaw.replace(/\\/g, "/").replace(/\/+$/, "") || ".";
|
|
357
|
+
const entries = [];
|
|
358
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
359
|
+
if (typeof key !== "string" || !key.includes("*")) continue;
|
|
360
|
+
const targets = Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
361
|
+
if (targets.length === 0) continue;
|
|
362
|
+
entries.push([key, targets]);
|
|
363
|
+
}
|
|
364
|
+
if (entries.length === 0) return null;
|
|
365
|
+
return {
|
|
366
|
+
entries,
|
|
367
|
+
baseUrl
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 用别名表解析一个模块名 → workspace 内相对路径(不含扩展名,`*` 已替换)。
|
|
372
|
+
* 返回 null = 该模块名不匹配任何别名模式(调用方继续走相对/bare 逻辑)。
|
|
373
|
+
*/
|
|
374
|
+
function resolveAliasModule(mapping, moduleName) {
|
|
375
|
+
for (const [pattern, targets] of mapping.entries) {
|
|
376
|
+
const starIdx = pattern.indexOf("*");
|
|
377
|
+
const prefix = pattern.slice(0, starIdx);
|
|
378
|
+
const suffix = pattern.slice(starIdx + 1);
|
|
379
|
+
if (!moduleName.startsWith(prefix) || !moduleName.endsWith(suffix)) continue;
|
|
380
|
+
const wildcard = moduleName.slice(prefix.length, moduleName.length - suffix.length);
|
|
381
|
+
if (wildcard.includes("*")) continue;
|
|
382
|
+
for (const target of targets) {
|
|
383
|
+
const t = target.replace(/^\.\//, "");
|
|
384
|
+
const base = mapping.baseUrl === "." ? "" : `${mapping.baseUrl}/`;
|
|
385
|
+
const resolved = `${base}${t.replace(/\*/g, wildcard)}`;
|
|
386
|
+
return resolved.replace(/\/+/g, "/");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
//#endregion
|
|
393
|
+
export { IMPORT_STATEMENT_TYPES, applyTodoMarkers, collectHeritageTargets, extractImportSources, extractSymbolName, maybeCollectCallSite, parseTsconfigPaths, resolveAliasModule, resolveCallEdges };
|
|
394
|
+
//# sourceMappingURL=treeSitterExtractors-B1ybKNrC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"treeSitterExtractors-B1ybKNrC.js","names":["IMPORT_STATEMENT_TYPES: ReadonlySet<string>","node: TreeSitterNode","out: string[]","raw: string | undefined","declaration: TreeSitterNode","out: Array<{ clauseKind: 'extends' | 'implements'; targetName: string }>","t: string","c: TreeSitterNode","CALL_EXPRESSION_TYPES: ReadonlySet<string>","NAME_NODE_TYPES: ReadonlySet<string>","TAIL_FIELD_BY_TYPE: Readonly<Record<string, string>>","INNER_FIELD_BY_TYPE: Readonly<Record<string, string>>","depth: number","nodeType: string","hostId: string | undefined","hostType: string | undefined","TODO_MARKERS: ReadonlyArray<readonly [marker: 'TODO' | 'FIXME', tag: string, countKey: string]>","parentNode: { tags?: string[]; properties: Record<string, unknown> }","matches: readonly string[]","symbols: readonly { readonly label: string; readonly id: string }[]","input: CallResolutionInput","edges: ResolvedCallEdge[]","importedIndexes: Array<Map<string, string[]>>","calleeId: string | null","evidence: EvidenceGrade | null","crossHit: string | null","grade: EvidenceGrade","jsonText: string","parsed: Record<string, unknown>","entries: Array<readonly [string, readonly string[]]>","mapping: TsconfigPathsMapping","moduleName: string"],"sources":["../src/adapters/treeSitterExtractors.ts"],"sourcesContent":["import type { TreeSitterNode } from './TreeSitterAdapter.ts';\nimport type { CallSite, EvidenceGrade } from '../types/index.ts';\n\n/**\n * 语法事实提取器集合(模块级纯函数,复杂度独立记账)。\n *\n * 归属理由:`TreeSitterAdapter.walk` 与 `FileSystemAdapter.fetchDataWithSymbols`\n * 的复杂度都已逼近预算上限(band-detector 口径),因此凡是「读一棵语法树 / 读一张\n * 表给出决策」的逻辑都放在本文件——它们不碰 fs、不碰 logger、可被单独测试。\n *\n * 本文件当前承载四组能力:\n * 1. import 模块名提取(cycle-129 · T1)——{@link IMPORT_STATEMENT_TYPES} /\n * {@link extractImportSources};\n * 2. 继承目标提取(cycle-123)——{@link collectHeritageTargets};\n * 3. tsconfig paths 别名解析(cycle-130 · T1)——{@link parseTsconfigPaths} /\n * {@link resolveAliasModule};\n * 4. **调用点提取与 call 边解析(T2)**——{@link CALL_EXPRESSION_TYPES} /\n * {@link extractCalleeName} / {@link maybeCollectCallSite} /\n * {@link resolveCallEdges}。修前 `src/adapters` 全目录 `call_expression` 命中数为 0,\n * 原生通道产出的 `call` 边恒为 0;而 `DEFAULT_PATH_EDGE_TYPES`\n * (`src/shared/analysis/XuanjiAdapter.ts`)含 `'call'`——于是上游 dsh-codegraph\n * 缺席时,影响面 / 调用链 / 环检测全部读到空图。\n */\n\n/**\n * import 语句节点类型(cycle-129 · T1):四类覆盖 TS/JS/Python/Rust/Java 的\n * import 语法形态。walk 拦截这些节点后调 {@link extractImportSources}。\n */\nexport const IMPORT_STATEMENT_TYPES: ReadonlySet<string> = new Set([\n 'import_statement', 'import_declaration', 'import_from_statement', 'use_declaration',\n]);\n\n/**\n * 从一个 import 语法节点提取模块名(模块级纯函数,复杂度独立记账)。\n * 语法形态与降级规则见 walk 内注释(TreeSitterAdapter.ts cycle-129 T1 段)。\n */\nexport function extractImportSources(node: TreeSitterNode): string[] {\n const out: string[] = [];\n const push = (raw: string | undefined): void => {\n if (!raw) return;\n // TS/JS string 节点带引号;统一剥引号后按长度过滤(空/过短跳过)。\n const cleaned = raw.replace(/^['\"`]|['\"`]$/g, '').trim();\n if (cleaned.length > 1) out.push(cleaned);\n };\n const sourceNode = node.childForFieldName?.('source');\n if (sourceNode) {\n push(sourceNode.text);\n return out;\n }\n const moduleName = node.childForFieldName?.('module_name')\n ?? node.childForFieldName?.('name')\n ?? node.childForFieldName?.('argument');\n if (moduleName) {\n // Python relative_import 的 module_name 有点前缀(`.stlib`)——剥点后\n // 为空则跳过(纯相对导入 `from . import x` 无名字可记)。\n const text = moduleName.text.replace(/^\\.+/, '').trim();\n if (text.length > 1) out.push(text);\n }\n return out;\n}\n\nexport function collectHeritageTargets(\n declaration: TreeSitterNode,\n): Array<{ clauseKind: 'extends' | 'implements'; targetName: string }> {\n const out: Array<{ clauseKind: 'extends' | 'implements'; targetName: string }> = [];\n const childByType = (t: string) => declaration.children?.find((c: TreeSitterNode) => c.type === t);\n\n // 形态一:interface 的 extends_type_clause\n const extendsClause = childByType('extends_type_clause');\n if (extendsClause) {\n for (const c of extendsClause.children ?? []) {\n if (c.type === 'type_identifier' && c.text) {\n out.push({ clauseKind: 'extends', targetName: c.text });\n }\n }\n }\n\n // 形态二:class 的 class_heritage(extends_clause + implements_clause)\n const heritage = childByType('class_heritage');\n if (heritage) {\n for (const clause of heritage.children ?? []) {\n const kind = clause.type === 'extends_clause' ? 'extends'\n : clause.type === 'implements_clause' ? 'implements' : null;\n if (!kind) continue;\n for (const c of clause.children ?? []) {\n if ((c.type === 'type_identifier' || c.type === 'identifier') && c.text) {\n out.push({ clauseKind: kind, targetName: c.text });\n }\n }\n }\n }\n\n // 形态三:Python 的 class_definition(superclasses 字段)\n const superclasses = declaration.childForFieldName?.('superclasses');\n if (superclasses) {\n for (const c of superclasses.children ?? []) {\n if ((c.type === 'identifier' || c.type === 'attribute') && c.text) {\n out.push({ clauseKind: 'extends', targetName: c.text });\n }\n }\n }\n return out;\n};\n\n// ── 调用点提取(T2)────────────────────────────────────────────────────────\n//\n// 六种 grammar 的调用语法节点(**全部实测**:探针脚本对每种 grammar 打印了\n// 节点类型与相应字段,不是按文档猜的)。三组形态:\n//\n// 1) typescript / tsx / javascript / rust / go → 节点 `call_expression`,\n// 被调方在 `function` 字段(裸标识符、成员/字段/选择/scoped 表达式、带泛型的包装节点);\n// 2) python → 节点 `call`,被调方在 `function` 字段(identifier / attribute);\n// 3) java → 节点 `method_invocation`,被调方在 `name` 字段(可选 `object` 字段)。\n//\n// ⚠️ 任务书写的「四类同族节点」并不准确:python 的节点类型是 `call`,\n// java 的是 `method_invocation`。以实测为准(本文件即证据)。\n\n/**\n * 调用表达式节点类型集合(跨 grammar 的并集)。\n * walk 拦截这些节点后交给 {@link maybeCollectCallSite} / {@link extractCalleeName}。\n */\nexport const CALL_EXPRESSION_TYPES: ReadonlySet<string> = new Set([\n 'call_expression', // typescript / tsx / javascript / rust / go\n 'call', // python\n 'method_invocation', // java\n]);\n\n/**\n * 名字节点的类型集合:这些节点的 `text` 就是末段标识符本身。\n * go / rust 的 `field_identifier`、TS 的 `private_property_identifier`\n * (`this.#p()` 实测)都在此列。\n */\nconst NAME_NODE_TYPES: ReadonlySet<string> = new Set([\n 'identifier', 'property_identifier', 'field_identifier', 'private_property_identifier',\n]);\n\n/**\n * 末段字段表:节点类型 → 直接保存末段名字的字段名。\n *\n * 用查表替代 switch 是有意的(复杂度预算):12 个 case 的分支数等于 12 个 if,\n * 而这里的三张表各占一行。六种 grammar 实测的字段归属如下——\n * member_expression 的末段在 `property`,python attribute 在 `attribute`,\n * rust scoped_identifier 在 `name`,rust field_expression 与 go selector_expression\n * 都在 `field`。\n */\nconst TAIL_FIELD_BY_TYPE: Readonly<Record<string, string>> = Object.freeze({\n member_expression: 'property',\n attribute: 'attribute',\n scoped_identifier: 'name',\n field_expression: 'field',\n selector_expression: 'field',\n});\n\n/**\n * 包装节点表:节点类型 → 内层表达式所在的字段名。\n * rust 的实例化调用 `o.m::<i32>()` 外面套 `generic_function`(内层在 `function`),\n * go 的 `foo[int]()` / `obj.M[int]()` 套 `index_expression`(内层在 `operand`,\n * `index` 是类型实参而非函数表达式)。\n */\nconst INNER_FIELD_BY_TYPE: Readonly<Record<string, string>> = Object.freeze({\n generic_function: 'function',\n index_expression: 'operand',\n});\n\n/** 取节点末段的**名字子节点**(不是文本切分——文本切分会被泛型/可选链带偏)。 */\nfunction lastNamedChild(node: TreeSitterNode): TreeSitterNode | null {\n return [...(node.children ?? [])].reverse().find((c) => NAME_NODE_TYPES.has(c.type)) ?? null;\n}\n\n/**\n * 从「被调方表达式」节点取简单名。六种 grammar 实测形态:\n *\n * - 裸标识符(TS/Py/Rust/Go 的直接调用)→ 自身 text;\n * - 末段字段表命中的包装节点 → 该字段;字段缺席时退回最后一个名字子节点\n * (可选链 `a?.b` 的子节点是 `identifier / optional_chain / property_identifier`,\n * 字段仍然可读,但退回路径覆盖语法恢复产出的异常形态);\n * - 包装节点表命中的 → 递归内层表达式;\n * - 括号表达式 `(f)()` → 递归唯一非括号子节点;\n * - TS `arr[0]()` 的 `subscript_expression` **刻意不取名**:下标不是符号引用,\n * 硬取会把 `number` 的文本当符号名(宁缺勿造)。\n *\n * java 的 `method_invocation` 没有被调方字段,直接读 `name`\n * (`obj.<T>g()` 实测 `name=g`,泛型参数不影响)。\n */\nfunction calleeFromExpression(node: TreeSitterNode, depth: number): string | null {\n // 深度上限防御病态 AST(语法错误恢复时可能出现自嵌套);正常代码远达不到。\n if (depth > 6) return null;\n if (NAME_NODE_TYPES.has(node.type)) return node.text || null;\n\n const tailField = TAIL_FIELD_BY_TYPE[node.type];\n if (tailField) {\n return node.childForFieldName?.(tailField)?.text || lastNamedChild(node)?.text || null;\n }\n\n const innerField = INNER_FIELD_BY_TYPE[node.type];\n const inner = innerField\n ? node.childForFieldName?.(innerField)\n : node.type === 'parenthesized_expression'\n ? (node.children ?? []).find((c) => c.type !== '(' && c.type !== ')')\n : null;\n return inner ? calleeFromExpression(inner, depth + 1) : null;\n}\n\n/**\n * 从调用语法节点提取被调方的**简单名**(末段标识符);无名字返回 null。\n *\n * 「简单名」是刻意的上限:本通道没有类型信息,`a.b()` 只能知道「有个叫 b 的调用」,\n * 不能知道 `a` 到底是什么类型。因此下游解析必须按「同名符号是否唯一」裁决,\n * 多义时**跳过而不是猜**(见 {@link resolveCallEdges})。\n */\nexport function extractCalleeName(node: TreeSitterNode): string | null {\n // java:`name` 字段直接是末段(`function` 字段不存在)。\n const nameField = node.childForFieldName?.('name');\n if (node.type === 'method_invocation') {\n return nameField?.text || null;\n }\n const callee = node.childForFieldName?.('function');\n if (!callee) return null;\n return calleeFromExpression(callee, 0);\n}\n\n/**\n * 收集一个调用点(若它的宿主符号合法)。\n *\n * ## 为什么必须要求宿主是可调用符号\n *\n * `walk` 用栈遍历,压栈时携带的 `parentId` 是**最近的一个符号节点**——它可能是\n * class、variable,也可能是 module(`import_statement` 本身就是一个 module 符号)。\n * 模块顶层的 `foo()` 若记成 `file → foo` 的 call 边,等于宣称「这个文件调用了 foo」;\n * `const x = foo()` 若记成 `x → foo`,等于宣称「变量 x 调用了 foo」。这两类边都会\n * 让调用链读法出现不存在的路径,所以:**宿主不是 function 类型就直接跳过**(宁缺勿造)。\n * 代价是模块级初始化调用(`const db = openDb()`)不产生 call 边——这是刻意接受的缺口。\n *\n * 本函数同时承担「调用节点 → 调用点」的判定,walk 里只用一行调用 + 一行 push,\n * 不再新增条件语句(复杂度预算:`walk` 所在文件已贴近 100)。\n *\n * @returns 新的调用点,或 null(不是调用节点 / 宿主非法 / 无名调用)。\n */\nexport function maybeCollectCallSite(\n node: TreeSitterNode,\n nodeType: string,\n hostId: string | undefined,\n hostType: string | undefined,\n): CallSite | null {\n if (!CALL_EXPRESSION_TYPES.has(nodeType)) return null;\n if (!hostId || hostType !== 'function') return null;\n const calleeName = extractCalleeName(node);\n if (!calleeName) return null;\n return {\n callerId: hostId as CallSite['callerId'],\n calleeName,\n line: node.startPosition.row + 1,\n };\n}\n\n/** TODO/FIXME 两种标记的字面量与写入字段(一张表替代四段重复分支)。 */\nconst TODO_MARKERS: ReadonlyArray<readonly [marker: 'TODO' | 'FIXME', tag: string, countKey: string]> = [\n ['TODO', 'todo', 'todoCount'],\n ['FIXME', 'fixme', 'fixmeCount'],\n];\n\n/**\n * 注释里的 TODO/FIXME 标记 → 打到宿主符号节点上(cycle-122 的 todoMarkerMode 数据面)。\n *\n * 从 `TreeSitterAdapter.walk` 里搬出来的原因纯粹是**复杂度预算**:那一块原有 5 个条件\n * 语句,而 walk 本身已到 101。搬动时**必须逐行等价**,否则「重构」会悄悄改掉行为——\n * 这里特意保留原实现的两处容易改错的语义:\n *\n * 1. **计数是累加**(同一宿主下多条注释各 +1),标签只加一次(已有就不重复加);\n * 2. `hasTodoFixme` 的判据是「命中了**任意**标记」,包括没有专属标签的 HACK / XXX / BUG\n * ——所以它写在循环**外面**。挪进循环体会让「只有 HACK 的注释」不再置位,\n * 而 `TODO_PATTERNS` 里 HACK/XXX/BUG 确实是合法命中项。\n *\n * 无标记命中时不碰 `tags`(保持 undefined,而不是写一个空数组):原实现只在需要\n * 追加时才赋值,`tags: []` 与 `tags: undefined` 在节点上是两种可观测状态。\n */\nexport function applyTodoMarkers(\n parentNode: { tags?: string[]; properties: Record<string, unknown> },\n matches: readonly string[],\n): void {\n const tags = parentNode.tags ?? [];\n for (const [marker, tag, countKey] of TODO_MARKERS) {\n if (!matches.includes(marker)) continue;\n if (!tags.includes(tag)) tags.push(tag);\n parentNode.properties[countKey] = ((parentNode.properties[countKey] as number) ?? 0) + 1;\n }\n if (tags.length > 0) parentNode.tags = tags;\n // 「命中任意标记」而不是「命中 TODO 或 FIXME」——见上面第 2 条。\n parentNode.properties.hasTodoFixme = true;\n}\n\n/**\n * 符号节点的名字(label)提取:`name` 字段 → `type` 字段(Rust `impl_item` 没有\n * `name`,它的 `type` 字段是被实现的类型,如 `Bar`)→ 第一个带名字的子节点。\n *\n * 六种 grammar 实测:符号节点的标签子节点总是 `identifier` / `property_identifier` /\n * `type_identifier` 之一。三者都没有的节点保留显式 `<node.type>` 标记——\n * `arrow_function` / `closure_expression` 没有名字,本来就该这样呈现。\n */\nexport function extractSymbolName(node: TreeSitterNode): string {\n const nameNode = node.childForFieldName?.('name') ||\n node.childForFieldName?.('type') ||\n node.children?.find((c: TreeSitterNode) =>\n c.type === 'identifier' || c.type === 'property_identifier' || c.type === 'type_identifier');\n return nameNode ? nameNode.text : `<${node.type}>`;\n}\n\n// ── call 边两跳解析(T2)──────────────────────────────────────────────────\n//\n// 证据分级(诚实是这里的核心要求,不是装饰):\n//\n// hop 1(exact):被调名在同一文件的符号集里**唯一**命中 ⇒ 名字→符号无歧义。\n// hop 2(inferred):同文件未命中,但在本文件已解析出的 import 目标文件里\n// **唯一**同名符号 ⇒ 边成立,但引入了 import 图这一额外前提。\n// 同文件多义 ⇒ 跳过(计入 ambiguous);都没有 ⇒ 跳过。\n//\n// 为什么多义必须跳过:同名符号 >1 时没有任何信息能选中其中一个,任意选一条都会\n// 制造一条看似确定、实为编造的边——那正是「影响面视图给出错误答案」的来源,\n// 比缺一条边严重得多。\n\n/** `resolveCallEdges` 的单文件输入(全部由调用方在 workspace 上下文中准备好)。 */\nexport interface CallResolutionInput {\n /** 本文件的调用点。 */\n readonly callSites: readonly CallSite[];\n /** 本文件名 → 文件内符号(label + id)。`filePath` 为本文件绝对路径。 */\n readonly localSymbols: readonly { readonly label: string; readonly id: string }[];\n /**\n * import 目标文件(绝对路径)→ 该文件的符号列表。只应包含**本文件确实 import 到**\n * 的文件;跨文件唯一命中才允许产出 inferred 边。\n */\n readonly importedFileSymbols: ReadonlyMap<string, readonly { readonly label: string; readonly id: string }[]>;\n}\n\n/**\n * 一条已解析的 call 边(原始 id 字符串,由调用方套 branded type)。\n *\n * `edgeOrigin` 与 `evidence` 是**两个不同的问题**,所以两个字段都在:\n * - `edgeOrigin` 回答「这条边的两端是怎么来的」,取值域沿用既有\n * `EDGE_ORIGIN_RANK`(`src/merger/GraphDataMerger.ts`):exact 边是语法树里\n * 同一个文件的父子级事实 ⇒ `'ast'`;inferred 边靠 import 图 + 名字唯一性\n * 推断出目标符号,**不是**语法树直接给出的 ⇒ `'heuristic'`(若也写 `'ast'`,\n * 就等于宣称「编辑器级精确」而实际只是名字匹配,比缺边更糟)。\n * - `evidence` 回答「名字→符号的映射有多确定」,是本轮新增的两级语义\n * ({@link EvidenceGrade})。\n */\nexport interface ResolvedCallEdge {\n readonly id: string;\n readonly source: string;\n readonly target: string;\n readonly properties: {\n readonly edgeOrigin: 'ast' | 'heuristic';\n readonly evidence: EvidenceGrade;\n readonly calleeName: string;\n readonly line: number;\n };\n}\n\n/** `resolveCallEdges` 的输出:边 + 本文件解析统计(统计是给日志与测试断言用的)。 */\nexport interface CallResolutionResult {\n readonly edges: ResolvedCallEdge[];\n /** 同文件多义而跳过的调用点数(**防空转的核心计数**)。 */\n readonly ambiguous: number;\n /** 两跳都没命中而跳过的调用点数。 */\n readonly unresolved: number;\n /** 实际落成的边按证据分级的计数(exact / inferred)。 */\n readonly byEvidence: { readonly exact: number; readonly inferred: number };\n}\n\n/** 按 label 建索引(同名多值保留全部,供唯一性判定)。 */\nfunction indexByLabel(\n symbols: readonly { readonly label: string; readonly id: string }[],\n): Map<string, string[]> {\n const map = new Map<string, string[]>();\n for (const symbol of symbols) {\n map.set(symbol.label, [...(map.get(symbol.label) ?? []), symbol.id]);\n }\n return map;\n}\n\n/**\n * 把一批调用点解析成 `call` 边(含证据分级)。\n *\n * 边 id 形如 `tscall:${callerId}:${calleeId}`——与既有 `tsimport:` / `tsedge:`\n * 前缀风格一致。同一对 (caller, callee) 出现多次调用只产出一条边\n * (调用次数不是图结构;重复边会被 merger 按 id 去重,但这里先去重可以省掉\n * 一次属性并集,也避免 ambiguous/unresolved 统计被重复调用放大)。\n */\nexport function resolveCallEdges(input: CallResolutionInput): CallResolutionResult {\n const edges: ResolvedCallEdge[] = [];\n const seenEdgeIds = new Set<string>();\n let ambiguous = 0;\n let unresolved = 0;\n let exactCount = 0;\n let inferredCount = 0;\n\n const localIndex = indexByLabel(input.localSymbols);\n const importedIndexes: Array<Map<string, string[]>> = [];\n for (const symbols of input.importedFileSymbols.values()) {\n importedIndexes.push(indexByLabel(symbols));\n }\n\n for (const site of input.callSites) {\n const localHits = localIndex.get(site.calleeName) ?? [];\n\n let calleeId: string | null = null;\n let evidence: EvidenceGrade | null = null;\n\n if (localHits.length > 1) {\n // 同文件多义:不猜。\n ambiguous++;\n continue;\n }\n if (localHits.length === 1) {\n calleeId = localHits[0] ?? null;\n evidence = 'exact';\n } else {\n // hop 2:跨文件唯一命中。任何一处多义都放弃(跨文件歧义比同文件更不可判)。\n let crossHit: string | null = null;\n let crossAmbiguous = false;\n for (const index of importedIndexes) {\n const hits = index.get(site.calleeName) ?? [];\n if (hits.length > 1) { crossAmbiguous = true; break; }\n if (hits.length === 1) {\n if (crossHit !== null) { crossAmbiguous = true; break; }\n crossHit = hits[0] ?? null;\n }\n }\n if (crossAmbiguous) { ambiguous++; continue; }\n if (crossHit === null) { unresolved++; continue; }\n calleeId = crossHit;\n evidence = 'inferred';\n }\n\n if (!calleeId) { unresolved++; continue; }\n if (calleeId === String(site.callerId)) continue; // 自引用(递归)跳过\n\n const edgeId = `tscall:${String(site.callerId)}:${calleeId}`;\n if (seenEdgeIds.has(edgeId)) continue;\n seenEdgeIds.add(edgeId);\n const grade: EvidenceGrade = evidence ?? 'inferred';\n if (grade === 'exact') exactCount++;\n else inferredCount++;\n edges.push({\n id: edgeId,\n source: String(site.callerId),\n target: calleeId,\n properties: {\n // exact = 语法树同一文件内的直接事实(ast 档);inferred = 名字 + import 图推断(heuristic 档)。\n edgeOrigin: grade === 'exact' ? 'ast' : 'heuristic',\n evidence: grade,\n calleeName: site.calleeName,\n line: site.line,\n },\n });\n }\n\n return { edges, ambiguous, unresolved, byEvidence: { exact: exactCount, inferred: inferredCount } };\n}\n\n/**\n * tsconfig paths 别名解析(cycle-130 · T1:现代 TS 项目的 import 图缺口)。\n *\n * `@/utils/helper` 形态的 import 依赖 tsconfig 的 `compilerOptions.paths`\n * 才能解析到真实文件——否则原生依赖图在这些项目上是半空的(cycle-129 登记缺口)。\n *\n * 解析器是**纯函数**(别名表进、映射决策出),tsconfig 的读取与缓存由\n * FileSystemAdapter 负责——本文件不碰 fs,复杂度独立记账。\n *\n * paths 形态(tsconfig 实测):`\"@/*\": [\"./src/*\"]`——key 是模式(单 `*`),\n * value 是候选列表(首个命中胜出)。value 相对 baseUrl(缺省 = tsconfig 所在\n * 目录)。本实现覆盖单星模式;多星/多候选按序试(宁缺勿造)。\n */\nexport interface TsconfigPathsMapping {\n /** 别名模式 → 候选目标模式列表(均含至多一个 `*`)。 */\n readonly entries: ReadonlyArray<readonly [string, readonly string[]]>;\n /** baseUrl(相对 tsconfig 所在目录,posix;无尾斜杠;'.' = 同目录)。 */\n readonly baseUrl: string;\n}\n\n/** 从 tsconfig JSON 文本提取 paths 映射(宽松解析:JSONC 注释/尾逗号容错)。 */\nexport function parseTsconfigPaths(jsonText: string): TsconfigPathsMapping | null {\n // JSONC 容错:剥块注释与行注释、尾逗号(tsconfig 常带注释与尾逗号)。\n const cleaned = jsonText\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/(^|\\s)\\/\\/[^\\n]*/g, '$1')\n .replace(/,(\\s*[}\\]])/g, '$1');\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(cleaned) as Record<string, unknown>;\n } catch {\n return null;\n }\n const compilerOptions = parsed.compilerOptions as Record<string, unknown> | undefined;\n if (!compilerOptions || typeof compilerOptions !== 'object') return null;\n const paths = compilerOptions.paths as Record<string, unknown> | undefined;\n if (!paths || typeof paths !== 'object') return null;\n const baseUrlRaw = typeof compilerOptions.baseUrl === 'string' ? compilerOptions.baseUrl : '.';\n const baseUrl = baseUrlRaw.replace(/\\\\/g, '/').replace(/\\/+$/, '') || '.';\n const entries: Array<readonly [string, readonly string[]]> = [];\n for (const [key, value] of Object.entries(paths)) {\n if (typeof key !== 'string' || !key.includes('*')) continue; // 单星模式才有通配语义\n const targets = Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : [];\n if (targets.length === 0) continue;\n entries.push([key, targets]);\n }\n if (entries.length === 0) return null;\n return { entries, baseUrl };\n}\n\n/**\n * 用别名表解析一个模块名 → workspace 内相对路径(不含扩展名,`*` 已替换)。\n * 返回 null = 该模块名不匹配任何别名模式(调用方继续走相对/bare 逻辑)。\n */\nexport function resolveAliasModule(mapping: TsconfigPathsMapping, moduleName: string): string | null {\n for (const [pattern, targets] of mapping.entries) {\n const starIdx = pattern.indexOf('*');\n const prefix = pattern.slice(0, starIdx);\n const suffix = pattern.slice(starIdx + 1);\n if (!moduleName.startsWith(prefix) || !moduleName.endsWith(suffix)) continue;\n const wildcard = moduleName.slice(prefix.length, moduleName.length - suffix.length);\n if (wildcard.includes('*')) continue;\n for (const target of targets) {\n // 目标相对 baseUrl(tsconfig 惯例);`./` 前缀归一。\n const t = target.replace(/^\\.\\//, '');\n const base = mapping.baseUrl === '.' ? '' : `${mapping.baseUrl}/`;\n const resolved = `${base}${t.replace(/\\*/g, wildcard)}`;\n return resolved.replace(/\\/+/g, '/');\n }\n }\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/types/index.ts
|
|
2
|
+
const RepoId = (id) => {
|
|
3
|
+
if (!id || id.length === 0) throw new Error("RepoId: id must not be empty");
|
|
4
|
+
return id;
|
|
5
|
+
};
|
|
6
|
+
const NodeId = (id) => {
|
|
7
|
+
if (!id || id.length === 0) throw new Error("NodeId: id must not be empty");
|
|
8
|
+
return id;
|
|
9
|
+
};
|
|
10
|
+
const EdgeId = (id) => {
|
|
11
|
+
if (!id || id.length === 0) throw new Error("EdgeId: id must not be empty");
|
|
12
|
+
return id;
|
|
13
|
+
};
|
|
14
|
+
function safeString(value, fallback = "") {
|
|
15
|
+
return typeof value === "string" ? value : fallback;
|
|
16
|
+
}
|
|
17
|
+
function safeNumber(value, fallback = 0) {
|
|
18
|
+
return typeof value === "number" && !Number.isNaN(value) ? value : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { EdgeId, NodeId, RepoId, safeNumber, safeString };
|
|
23
|
+
//# sourceMappingURL=types-BUDxI_00.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-BUDxI_00.js","names":["id: string","value: unknown"],"sources":["../src/types/index.ts"],"sourcesContent":["// Branded types and graph data structures shared across the Host/Client boundary.\r\n// Graph heat-update events are declared here via Cordis declaration merging.\r\n\r\nexport type Branded<T, Brand> = T & { __brand: Brand };\r\n\r\nexport type RepoId = Branded<string, 'RepoId'>;\r\nexport type NodeId = Branded<string, 'NodeId'>;\r\nexport type EdgeId = Branded<string, 'EdgeId'>;\r\n\r\nexport const RepoId = (id: string): RepoId => {\r\n if (!id || id.length === 0) throw new Error('RepoId: id must not be empty');\r\n return id as RepoId;\r\n};\r\nexport const NodeId = (id: string): NodeId => {\r\n if (!id || id.length === 0) throw new Error('NodeId: id must not be empty');\r\n return id as NodeId;\r\n};\r\nexport const EdgeId = (id: string): EdgeId => {\r\n if (!id || id.length === 0) throw new Error('EdgeId: id must not be empty');\r\n return id as EdgeId;\r\n};\r\n\r\nconst NODE_TYPES = ['function', 'class', 'variable', 'module', 'interface', 'type'] as const;\r\n\r\nexport function isNodeType(value: unknown): value is GraphNode['type'] {\r\n return typeof value === 'string' && (NODE_TYPES as readonly string[]).includes(value);\r\n}\r\n\r\nexport function safeString(value: unknown, fallback = ''): string {\r\n return typeof value === 'string' ? value : fallback;\r\n}\r\n\r\nexport function safeNumber(value: unknown, fallback = 0): number {\r\n return typeof value === 'number' && !Number.isNaN(value) ? value : fallback;\r\n}\r\n\r\nexport function safeBoolean(value: unknown, fallback = false): boolean {\r\n return typeof value === 'boolean' ? value : fallback;\r\n}\r\n\r\nexport type HeatmapMode = 'none' | 'instability' | 'lines' | 'complexity';\r\nexport type TodoMarkerMode = 'none' | 'todo' | 'fixme' | 'all';\r\n\r\nexport interface GraphNode {\r\n id: NodeId;\r\n label: string;\r\n type: 'function' | 'class' | 'variable' | 'module' | 'interface' | 'type';\r\n filePath: string;\r\n lineNumber?: number;\r\n properties: Record<string, unknown>;\r\n parentId?: NodeId;\r\n summary?: string;\r\n tags?: string[];\r\n complexity?: 'simple' | 'moderate' | 'complex';\r\n layer?: string;\r\n}\r\n\r\nexport type EdgeDirection = 'forward' | 'backward' | 'bidirectional';\r\n\r\nexport interface GraphEdge {\r\n id: EdgeId;\r\n source: NodeId;\r\n target: NodeId;\r\n type: 'call' | 'import' | 'extend' | 'implement' | 'dependency' | 'contains' | 'association' | 'aggregation' | 'composition';\r\n direction?: EdgeDirection;\r\n weight?: number;\r\n properties: Record<string, unknown>;\r\n}\r\n\r\nexport interface GraphData {\r\n nodes: GraphNode[];\r\n edges: GraphEdge[];\r\n metadata: {\r\n repoId: RepoId;\r\n timestamp: number;\r\n nodeCount: number;\r\n edgeCount: number;\r\n truncated?: boolean;\r\n totalNodeCount?: number;\r\n totalEdgeCount?: number;\r\n /**\r\n * 璇玑图增量引擎健康面(cycle-122,XJ-11 消费端收口)。\r\n *\r\n * 缺口现场:`xuanjiPool.applyDelta(...)` 的返回值(`PoolDeltaResult`:\r\n * applied / addedPaths / degradedRules / I4 不变量报告)在 `src/tools.ts`\r\n * 曾被**完全丢弃**——降级信号生产了但没有消费面,「不可观测的降级等于\r\n * 没有降级」(XJ-11 头注原文)。\r\n *\r\n * 字段在 `executeGraphData` 组装(增量分支才有;全量重建时为 undefined):\r\n * 模型读 `graph_data` 返回值即可判断「这次合并走的是增量还是全量、有没有\r\n * 规则被降级」——不再需要读源码才能知道引擎状态。\r\n */\r\n xuanji?: {\r\n /** delta 是否真的被应用到缓存适配层(false = 全量重建 / 尚未装载)。 */\r\n applied: boolean;\r\n /** 本轮增量新增的物化路径数(增量引擎的真实产出,量化可观测)。 */\r\n addedPaths?: number;\r\n /** 因超出展开预算被降级重建的规则名(空数组 = 无降级)。 */\r\n degradedRules?: readonly string[];\r\n /** I4(stale 比例)判为不达标的规则名。 */\r\n invariantViolations?: readonly string[];\r\n };\r\n /**\r\n * 继承数据增强结果(cycle-123,研究报告 Phase 1 P0 数据质量缺口)。\r\n *\r\n * 上游索引对 `interface extends` 检出为 0(本仓 17 处源码 extends 只出\r\n * 9 条 class→class)——AST 补扫在 merger 之后注入增强边。全量路径才有\r\n * 此字段(增量轮的缓存已含上轮增强);`applied: false` = 不可用/零增量\r\n * (宁缺勿造)。模型可据此向用户披露「继承视图的边含 AST 增补 N 条」。\r\n */\r\n inheritance?: {\r\n applied: boolean;\r\n extracted: number;\r\n added: number;\r\n ambiguous: number;\r\n unresolved: number;\r\n duplicates: number;\r\n filesScanned: number;\r\n durationMs: number;\r\n };\r\n /**\r\n * 图层来源声明(cycle-127 · T3):本次合并数据来自哪条通道。\r\n *\r\n * - `'upstream'`:上游 dsh-codegraph 索引(SQLite)参与合并;\r\n * - `'native'`:原生通道(目录结构 + TreeSitter 符号 + AST 边)——仅在\r\n * auto 降级链第三级(上游与 lens 双缺席)启用;\r\n * - `'empty'`:所有通道均无产出(headless / 工作区不可读),诚实空态。\r\n *\r\n * 「不可观测的降级等于没有降级」(XJ-11 家族):模型与客户端据此向用户\r\n * 披露数据来源,而不是把空图当作「工作区没有代码」。\r\n */\r\n graphLayer?: 'upstream' | 'native' | 'empty';\r\n /**\r\n * 原生索引命中信息(cycle-128 · T2):`graphLayer === 'native'` 且索引库\r\n * 命中时在场——模型/客户端可据此披露「毫秒级索引读取」vs「实时扫描」。\r\n * 实时扫描路径(回写后)不带此字段(hit 语义仅指本次图来自库)。\r\n */\r\n nativeIndex?: {\r\n hit: true;\r\n indexedAt: number;\r\n fingerprint: string;\r\n };\r\n };\r\n layers?: Layer[];\r\n}\r\n\r\nexport interface Layer {\r\n id: string;\r\n name: string;\r\n description?: string;\r\n nodeIds: NodeId[];\r\n color?: string;\r\n}\r\n\r\n/**\r\n * 视图模式。前 9 项是图谱视图(结构层 + 行为层),`runtime` 是 cycle-88 新增的\r\n * 运行时层(`docs/图谱按需加载.md:196-200` 的第三层:Trace / 日志 / 性能排查)。\r\n *\r\n * 运行时层刻意**不映射任何图谱装配器**:`src/client/diagram/index.ts` 的\r\n * `assembleDiagram` 走 `macroAssemblerRegistry.get(viewMode)`,未注册即返回\r\n * `null`,因此 `runtime` 天然得到空子图——这正是验收原文\r\n * `docs/图谱按需加载.md:202`「切换层时替换子图,而不是追加节点」要的结果。\r\n * 运行时层的数据面是进程内可观测数据(`Logger` 环形缓冲 +\r\n * `PerformanceMonitor`),不是图节点。\r\n */\r\nexport type ViewMode = 'code' | 'dependencies' | 'inheritance' | 'callgraph' | 'package' | 'class' | 'cfg' | 'flowchart' | 'sequence' | 'runtime';\r\n\r\n\r\nexport interface InheritanceNodeProperties {\r\n depth: number;\r\n isRoot: boolean;\r\n parentCount: number;\r\n childCount: number;\r\n descendantCount: number;\r\n isAbstract: boolean;\r\n}\r\n\r\nexport interface CallGraphNodeProperties {\r\n depth: number;\r\n isEntry: boolean;\r\n entryType: 'export' | 'handler' | 'event' | 'cli' | 'test' | 'none';\r\n inDegree: number;\r\n outDegree: number;\r\n isRecursive: boolean;\r\n}\r\n\r\nexport type DataSourceType = 'auto' | 'codegraph' | 'lens' | 'tree-sitter' | 'dependency';\r\n\r\n/**\r\n * 边的**证据分级**(本轮 T2:原生 `call` 边提取)。\r\n *\r\n * 为什么不是随便加一个 `confidence: number`:本轮的两跳解析只有两种可被代码证明的状态,\r\n * 一个连续量会让「一堆 0.7」既无法验证也无法断言。\r\n *\r\n * - `'exact'` —— 被调名在**同一个文件**的符号集里**唯一**命中。调用发生点与符号\r\n * 定义点在同一个语法单元内,名字→符号的映射没有歧义(仍然只是名字级解析,\r\n * 不是类型系统级解析,见 {@link CallSite} 的注释)。\r\n * - `'inferred'` —— 同文件未命中,靠**本文件已解析出的 import 目标文件**里的唯一同名符号\r\n * 补上。这一步引入了「import 图 + 名字唯一性」两个前提,任何一个错了边就错了,\r\n * 所以它必须与 exact 区分开,并落到 `edgeOrigin: 'heuristic'` 档(见\r\n * `src/adapters/treeSitterExtractors.ts` 的 `resolveCallEdges`)。\r\n *\r\n * 不放在 `src/xuanji/**`:那是零宿主依赖的库(有源码级守卫\r\n * `tests/unit/xuanji/libraryBoundary.test.ts`),图谱类型属于宿主。\r\n */\r\nexport type EvidenceGrade = 'exact' | 'inferred';\r\n\r\n/**\r\n * 一个**调用点**(语法事实):某符号体内出现了一次对被调简单名的调用。\r\n *\r\n * 本仓库反复强调的「适配器是语法事实收集者,不做语义裁决」在这里的落地方式:\r\n * 本类型只记录「谁(callerId)在行为上出现了一次叫 calleeName 的调用、在第几行」,\r\n * **不**记录被调方到底解析到了哪个符号(那是 `FileSystemAdapter` 需要 workspace\r\n * 目录清单与 import 图才能做的裁决)。\r\n *\r\n * 刻意只存**简单名**(末段标识符):`a.b()` 记 `b`、`self.foo()` 记 `foo`、`obj.M()` 记 `M`。\r\n * 不做「按接收者类型找方法」的猜测——那需要类型信息,本通道没有。\r\n */\r\nexport interface CallSite {\r\n /** 调用宿主符号的 id(最近的 function/method 祖先;模块顶层调用不产生 call site)。 */\r\n callerId: NodeId;\r\n /** 被调方的简单名(末段标识符)。 */\r\n calleeName: string;\r\n /** 调用点所在行(1-based,与 `GraphNode.lineNumber` 同域)。 */\r\n line: number;\r\n}\r\n\r\nexport interface AdapterResult {\r\n nodes: GraphNode[];\r\n edges: GraphEdge[];\r\n source: DataSourceType;\r\n timestamp: number;\r\n error?: string;\r\n /**\r\n * 文件 → 该文件 import 的模块名(cycle-129 · T1,仅 tree-sitter 源产出)。\r\n *\r\n * 语法事实(import 语句的 `source` 字符串)与语义裁决(模块名 → workspace\r\n * 内文件节点)刻意分离:TreeSitter 适配器保持「语法事实收集者」,路径解析\r\n * 由 FileSystemAdapter 统一裁决(需要目录清单上下文)。消费方按需取用;\r\n * 其它来源此字段缺席(不编造空对象)。\r\n */\r\n importsByFile?: Map<string, string[]>;\r\n /**\r\n * 文件 → 该文件的调用点列表(本轮 T2,仅 tree-sitter 源产出)。\r\n *\r\n * 与 `importsByFile` 同一条设计原则:TreeSitter 只交出**语法事实**(调用点),\r\n * 「被调名 → 哪个符号」的裁决与 `exact`/`inferred` 分级由 FileSystemAdapter\r\n * 在 workspace 上下文里完成。键域与 `importsByFile` 一致(绝对路径)。\r\n */\r\n callSitesByFile?: Map<string, CallSite[]>;\r\n}\r\n\r\n// ---- Graph heat-update event payloads -------------------------------------\r\n\r\n/**\r\n * Structural difference between two graph revisions (P2-21).\r\n *\r\n * Lives here rather than next to its producer so both the host diff engine and the client store can\r\n * name it without importing each other; `shared/graphDelta.ts` re-exports it and owns the helpers.\r\n */\r\nexport interface GraphDelta {\r\n addedNodes: GraphNode[];\r\n removedNodes: GraphNode[];\r\n modifiedNodes: GraphNode[];\r\n addedEdges: GraphEdge[];\r\n removedEdges: GraphEdge[];\r\n modifiedEdges: GraphEdge[];\r\n}\r\n\r\nexport interface GraphUpdatedEvent {\r\n repoId: RepoId;\r\n nodeCount: number;\r\n edgeCount: number;\r\n timestamp: number;\r\n /** P0-6 structural fingerprint; stable while graph content is unchanged. */\r\n fingerprint?: string;\r\n /** File paths that triggered an incremental re-scan, when the watcher drove the update. */\r\n changedFiles?: string[];\r\n /**\r\n * P2-21: this update is followed by a `graph/delta` carrying the actual changes, so consumers that\r\n * understand deltas must not re-fetch the whole graph in response to this event.\r\n */\r\n incremental?: boolean;\r\n}\r\n\r\nexport interface GraphDataEvent {\r\n repoId: RepoId;\r\n nodes: GraphNode[];\r\n edges: GraphEdge[];\r\n timestamp: number;\r\n /** P0-6 structural fingerprint; stable while graph content is unchanged. */\r\n fingerprint?: string;\r\n /** File paths that triggered an incremental re-scan, when the watcher drove the update. */\r\n changedFiles?: string[];\r\n /**\r\n * 璇玑图增量引擎健康信号(cycle-122):`executeGraphData` 组装、SSE 透传到\r\n * 客户端 StatusBar(XJ-11 消费端收口)。增量分支才有;全量重建缺省。\r\n */\r\n xuanji?: {\r\n applied: boolean;\r\n addedPaths?: number;\r\n degradedRules?: readonly string[];\r\n invariantViolations?: readonly string[];\r\n };\r\n}\r\n\r\n/**\r\n * 原生索引被删除(cycle-141 管理面板)。\r\n *\r\n * 删除成功后 host 会失效 `scanCache` / `scanInFlightMap` 并广播本事件,客户端据此\r\n * **丢弃陈旧状态**(清空图槽、重置 ETag 校验值)。只删文件不广播 = 用户看到一张死图。\r\n */\r\nexport interface GraphDeletedEvent {\r\n repoId: RepoId;\r\n path: string;\r\n timestamp: number;\r\n}\r\n\r\n/**\r\n * P2-21 targeted update: only the entries that actually changed since the consumer's revision.\r\n *\r\n * Consumers apply this with `applyGraphDelta()` and must check `canApplyGraphDelta()` first — a\r\n * delta is only meaningful against the exact revision it was computed from.\r\n */\r\nexport interface GraphDeltaEvent {\r\n repoId: RepoId;\r\n delta: GraphDelta;\r\n timestamp: number;\r\n /** P0-6 structural fingerprint of the resulting graph. */\r\n fingerprint?: string;\r\n /** File paths that triggered this update. */\r\n changedFiles?: string[];\r\n}\r\n\r\nexport interface RepoImportedEvent {\r\n repoId: RepoId;\r\n path: string;\r\n timestamp: number;\r\n}\r\n\r\nexport interface RepoScannedEvent {\r\n repoId: RepoId;\r\n fileCount: number;\r\n timestamp: number;\r\n}\r\n\r\nexport interface RepoRequestScanEvent {\r\n path: string;\r\n timestamp: number;\r\n}\r\n\r\nexport interface SourceOpenEvent {\r\n filePath: string;\r\n lineNumber: number;\r\n}\r\n\r\n/**\r\n * 能力位变更事件。\r\n *\r\n * `astSource` / `graphSource` 是两个**不同**的能力(`docs/需求分析.md` §61.10):\r\n * AST 分析通道存在 ≠ 能产出一张图。(cycle-118:`builtin` 兼容别名已移除,\r\n * 事件载荷不再携带;监听方读 `astSource` / `graphSource`。)\r\n */\r\nexport interface PrerequisiteStatusEvent {\r\n codegraph: boolean;\r\n lens: boolean;\r\n /** 内置 AST 分析通道是否可用(TreeSitter)。 */\r\n astSource: boolean;\r\n /** 内置图数据通道是否存在(manifest 依赖图)。 */\r\n graphSource: boolean;\r\n timestamp: number;\r\n}\r\n\r\nexport interface GraphInitEvent {\r\n path: string;\r\n timestamp: number;\r\n}\r\n\r\nexport interface GraphInitResultEvent {\r\n success: boolean;\r\n path: string;\r\n message: string;\r\n errorCode?: string;\r\n timestamp: number;\r\n}\r\n\r\nexport interface WatchToggleEvent {\r\n enabled: boolean;\r\n path: string;\r\n timestamp: number;\r\n}\r\n\r\nexport interface HealthScoreEvent {\r\n repoId: RepoId;\r\n score: HealthScore;\r\n timestamp: number;\r\n}\r\n\r\nexport interface RuleViolationEvent {\r\n violations: RuleViolation[];\r\n timestamp: number;\r\n}\r\n\r\nexport interface PathFoundEvent {\r\n path: PathResult;\r\n timestamp: number;\r\n}\r\n\r\nexport interface FocusChangedEvent {\r\n nodeId: NodeId | null;\r\n depth: number;\r\n timestamp: number;\r\n}\r\n\r\nexport interface HealthScore {\r\n coupling: number;\r\n cohesion: number;\r\n cyclicDependencyRate: number;\r\n modularity: number;\r\n overall: number;\r\n}\r\n\r\nexport type RuleType = 'forbidden-edge' | 'required-edge' | 'max-indegree' | 'max-outdegree' | 'cross-layer-call';\r\n\r\nexport interface RuleDefinition {\r\n id: string;\r\n name: string;\r\n type: RuleType;\r\n params: Record<string, unknown>;\r\n severity: 'error' | 'warning';\r\n enabled: boolean;\r\n}\r\n\r\nexport interface RuleViolation {\r\n ruleId: string;\r\n nodeId?: NodeId;\r\n edgeId?: EdgeId;\r\n message: string;\r\n severity: 'error' | 'warning';\r\n}\r\n\r\nexport interface PathResult {\r\n source: NodeId;\r\n target: NodeId;\r\n path: NodeId[];\r\n length: number;\r\n}\r\n\r\n// ---- Cordis event declaration merging ------------------------------------\r\n// These are emitted by the host plugin; Client/host listeners subscribe via\r\n// `ctx.on(...)`. Keep them writer-only so consumers never redefine the contract.\r\ndeclare module '@deepseek-ai/cordis' {\r\n interface Events {\r\n /**\r\n * @mode serial\r\n * @param event Repository imported into the workspace\r\n */\r\n 'codegraph-visualizer/repo/imported'(event: RepoImportedEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Repository scan completed with file count\r\n */\r\n 'codegraph-visualizer/repo/scanned'(event: RepoScannedEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Request to scan a repository at the given path\r\n */\r\n 'codegraph-visualizer/repo/request-scan'(event: RepoRequestScanEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Graph updated with new node/edge counts\r\n */\r\n 'codegraph-visualizer/graph/updated'(event: GraphUpdatedEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Full graph data payload for rendering\r\n */\r\n 'codegraph-visualizer/graph/data'(event: GraphDataEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Native index deleted; consumers must drop stale graph state\r\n */\r\n 'codegraph-visualizer/graph/deleted'(event: GraphDeletedEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Targeted graph update carrying only the entries that changed (P2-21)\r\n */\r\n 'codegraph-visualizer/graph/delta'(event: GraphDeltaEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Request to open a source file at a specific line\r\n */\r\n 'codegraph-visualizer/source/open'(event: SourceOpenEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Prerequisite service availability status changed\r\n */\r\n 'codegraph-visualizer/prerequisite/status'(event: PrerequisiteStatusEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Request prerequisite status check\r\n */\r\n 'codegraph-visualizer/prerequisite/request'(event: { timestamp: number }): void;\r\n /**\r\n * @mode serial\r\n * @param event Request to initialize graph for a repository path\r\n */\r\n 'codegraph-visualizer/graph/init'(event: GraphInitEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Graph initialization result (success/failure)\r\n */\r\n 'codegraph-visualizer/graph/init-result'(event: GraphInitResultEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event File watcher toggle state changed\r\n */\r\n 'codegraph-visualizer/watch/toggle'(event: WatchToggleEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event File watcher error occurred\r\n */\r\n 'codegraph-visualizer/watch/error'(event: { path: string; message: string; errorCode?: string; timestamp: number }): void;\r\n /**\r\n * @mode serial\r\n * @param event Architecture health score calculated\r\n */\r\n 'codegraph-visualizer/health/scored'(event: HealthScoreEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Architecture rule violation detected\r\n */\r\n 'codegraph-visualizer/rules/violation'(event: RuleViolationEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Shortest path found between two symbols\r\n */\r\n 'codegraph-visualizer/path/found'(event: PathFoundEvent): void;\r\n /**\r\n * @mode serial\r\n * @param event Focus changed to a different node (or null to clear)\r\n */\r\n 'codegraph-visualizer/focus/changed'(event: FocusChangedEvent): void;\r\n }\r\n interface Context {\r\n webServer: WebServerService;\r\n effect(execute: () => (() => void) | void, label?: string): () => void;\r\n emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void;\r\n on<K extends keyof Events>(name: K, listener: Events[K]): () => boolean;\r\n get(name: string, strict?: boolean): unknown;\r\n provide(name: string, service: unknown): () => void;\r\n /**\r\n * 响应式服务注入:**等到依赖到齐才跑回调**,且依赖被替换时会重跑\r\n * (旧 scope 先卸载、再跑新回调)。\r\n *\r\n * 契约与真实 cordis 对齐(cycle-83 修正,此前这里错写成\r\n * `callback: (injected: Map<string, unknown>) => void`,与实现不符):\r\n *\r\n * - `inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>`\r\n * (`<A>\\cordis\\lib\\types\\registry.d.ts:111` 与 `:185`)\r\n * - `interface Function<T> extends Base<T> { (ctx: Context, config: T): any }`\r\n * (`registry.d.ts:71-73`)⇒ **回调首参是 `Context`**,不是 Map\r\n * - JSDoc 逐字:「Run a callback once the requested services are available.\r\n * Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is\r\n * unloaded and re-run whenever a required service changes.」\r\n *\r\n * 返回值是**子 fiber**,随父 fiber 卸载自动释放,因此不需要手工 dispose。\r\n * 依赖永不到齐时回调永不执行,且不阻塞父插件 apply(cordis 探针实验 1 实测)。\r\n */\r\n inject(deps: string[], callback: (scope: Context, config?: unknown) => void): import('@deepseek-ai/cordis').Fiber & PromiseLike<import('@deepseek-ai/cordis').Fiber>;\r\n slots: {\r\n inject(slotName: string, callback: () => void): (() => void) | void;\r\n register(options: { name: string; id?: string; key?: string; order?: number; label?: () => string; inject?: (...args: unknown[]) => unknown }, component?: unknown): (() => void) | void;\r\n };\r\n betterSidebar?: {\r\n version: string;\r\n features: string[];\r\n registerTab(descriptor: {\r\n id: string;\r\n title: string | (() => string);\r\n description?: string | (() => string);\r\n icon?: unknown;\r\n order?: number;\r\n single?: boolean;\r\n hidden?: boolean;\r\n badge?: () => string | number | null | undefined;\r\n onOpen?: (tab: unknown, scope: unknown) => void;\r\n onActivate?: (tab: unknown, scope: unknown) => void;\r\n onClose?: (tab: unknown, scope: unknown) => void;\r\n component: (props: {\r\n ctx: Context;\r\n store: unknown;\r\n scope: unknown;\r\n tab: { id: string; title: string; path?: string };\r\n visible: boolean;\r\n expanded?: string[];\r\n revealed?: string[];\r\n }) => unknown;\r\n }): (() => void) | void;\r\n openTab(seed: { type: string; title?: string; path?: string; target?: 'right' | 'bottom' }): void;\r\n closeTab(tabId: string, scope?: unknown): void;\r\n activateTab(tabId: string, scope?: unknown): void;\r\n updateTab(tabId: string, patch: { title?: string; path?: string; meta?: unknown }): void;\r\n subscribe(listener: () => void): () => void;\r\n subscribeState(listener: () => void): () => void;\r\n getSnapshot(): {\r\n sessionId: string | undefined;\r\n state: {\r\n activePane: string | null;\r\n bottomOpen: boolean;\r\n bottomHeight: number;\r\n expanded: string[];\r\n revealed: string[];\r\n } | undefined;\r\n prefs: unknown;\r\n };\r\n };\r\n /**\r\n * DSH 斜杠命令贡献面(`@deepseek-ai/dsh-client-ui-commands`)。\r\n *\r\n * 契约逐字来源:`lib/types/client/contract.d.ts:129`\r\n * `register(contribution: CommandContribution): () => void`,JSDoc\r\n * 「Register one client command contribution; effect disposer. Duplicate names throw\r\n * at registration.」;`CommandContribution`(同文件 `:92-105`)必填\r\n * `name` 与 `available(session)`;`ActionSpec`(`:75-82`)为\r\n * `{ kind: 'action'; run(session): void }`。\r\n *\r\n * 本服务由 `Service` 基类 `super(ctx, 'commandUi')` 注册(gen2\r\n * `dsh-client-ui-commands/lib/client.js:771`、gen1 同文件 `:737`),因此两代都在。\r\n * 它替代了本项目 cycle-83 清除的 `spotlight` 幻影服务。\r\n */\r\n commandUi?: {\r\n register(contribution: {\r\n name: string;\r\n label?: () => string;\r\n description?: () => string;\r\n icon?: unknown;\r\n available: (session: unknown) => boolean;\r\n ui: { kind: 'action'; run: (session: unknown) => void };\r\n }): (() => void) | void;\r\n };\r\n\r\n /**\r\n * DSH 原生右侧栏的 tab 类型注册表(`@deepseek-ai/dsh-client-ui-sidebar-right`)。\r\n *\r\n * 形状逐字对齐 `lib/types/client/tab-registry.d.ts:75-120` 的\r\n * `SidebarRightTabDefinition`:\r\n * - `title: (address: string) => string` —— **必填**,且收 `address` 形参;\r\n * 旧声明写成可选且无形参,是契约不精确。\r\n * - `keepMounted?: boolean` —— 「Lazily keep a visited body mounted through hiding,\r\n * Session changes and docking; default false」,图表面板需要它。\r\n * - `multiple?: boolean` —— 缺省即「每个 pane 每 kind 一页」。\r\n * - **没有 `order` 字段** —— 排序由 `priority` band 与 `RANKS`\r\n * (`extension: 3 / builtin: 2 / fallback: 1`,见\r\n * `dsh-client-ui-sidebar-right/lib/client.js:8619-8623`)决定,旧声明多写了 `order`。\r\n */\r\n sidebarRightTabs?: {\r\n register(definition: {\r\n id: string;\r\n kind: string;\r\n title: (address: string) => string;\r\n multiple?: boolean;\r\n keepMounted?: boolean;\r\n patterns?: string[];\r\n priority?: 'extension' | 'builtin' | 'fallback';\r\n canOpen?: (address: string) => boolean;\r\n guide?: readonly { id: string; [key: string]: unknown }[];\r\n }): (() => void) | void;\r\n };\r\n\r\n workspaceRegistry?: {\r\n list?(): Array<{ path?: string }>;\r\n };\r\n\r\n hostVersion?: string;\r\n version?: string;\r\n app?: { version?: string };\r\n runtime?: { version?: string };\r\n }\r\n}\r\n\r\n// ---- DSH WebServer service (Host-side HTTP route registration) -----------\r\n\r\nexport interface IGraphVisualizerService {\r\n getCurrentGraph(): GraphData | null;\r\n getGraphData(repoId: RepoId): Promise<GraphData | null>;\r\n getSymbolDetail(symbolId: string): Promise<Record<string, unknown> | null>;\r\n getImpactAnalysis(symbolId: string): Promise<{ affected: string[]; depth: number } | null>;\r\n onGraphUpdate(callback: (data: GraphData) => void): () => void;\r\n}\r\n\r\nexport interface WebServerRoute {\r\n kind: 'exact' | 'prefix';\r\n path: string;\r\n handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;\r\n}\r\n\r\nexport interface WebServerService {\r\n register(route: WebServerRoute): () => void;\r\n registerFallback(handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>): () => void;\r\n}\r\n\r\n// Minimal Node.js HTTP types (avoid pulling in @types/node for client builds)\r\nexport interface IncomingMessage {\r\n url?: string;\r\n method?: string;\r\n headers: Record<string, string | string[] | undefined>;\r\n socket?: { remoteAddress?: string };\r\n on(event: 'data', handler: (chunk: Buffer) => void): void;\r\n on(event: 'end', handler: () => void): void;\r\n on(event: 'error', handler: (err: Error) => void): void;\r\n on(event: 'close', handler: () => void): void;\r\n destroy(): void;\r\n}\r\n\r\nexport interface ServerResponse {\r\n writeHead(code: number, headers?: Record<string, string>): void;\r\n write(data: string): boolean;\r\n end(data?: string): void;\r\n headersSent: boolean;\r\n}\r\n\r\n// ---- DSH Module Loader (runtime global) ----------------------------------\r\n// Injected by the DSH web app boot script. Client bundles register themselves\r\n// via __ModuleLoader__.load({ id, factory }) so the shell can wire them into\r\n// the page (slots, overlays, etc.). The factory receives a `require` function\r\n// for resolving platform externals (react, react-dom, cordis, etc.).\r\ninterface ModuleLoader {\r\n load(registration: { id: string; factory: (require: (id: string) => unknown) => Record<string, unknown> }): void;\r\n}\r\n\r\ndeclare const __ModuleLoader__: ModuleLoader | undefined;"],"mappings":";AASA,MAAa,SAAS,CAACA,OAAuB;AAC5C,MAAK,MAAM,GAAG,WAAW,EAAG,OAAM,IAAI,MAAM;AAC5C,QAAO;AACR;AACD,MAAa,SAAS,CAACA,OAAuB;AAC5C,MAAK,MAAM,GAAG,WAAW,EAAG,OAAM,IAAI,MAAM;AAC5C,QAAO;AACR;AACD,MAAa,SAAS,CAACA,OAAuB;AAC5C,MAAK,MAAM,GAAG,WAAW,EAAG,OAAM,IAAI,MAAM;AAC5C,QAAO;AACR;AAQD,SAAgB,WAAWC,OAAgB,WAAW,IAAY;AAChE,eAAc,UAAU,WAAW,QAAQ;AAC5C;AAED,SAAgB,WAAWA,OAAgB,WAAW,GAAW;AAC/D,eAAc,UAAU,aAAa,OAAO,MAAM,MAAM,GAAG,QAAQ;AACpE"}
|
package/locale/en.json
ADDED