@nudojs/service 0.2.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +27 -0
  3. package/dist/index.d.ts +638 -5
  4. package/dist/index.js +4439 -208
  5. package/package.json +17 -4
  6. package/src/__tests__/abs-bindings.test.ts +53 -0
  7. package/src/__tests__/abs-call-records.test.ts +33 -0
  8. package/src/__tests__/abs-calls.test.ts +59 -0
  9. package/src/__tests__/abs-module-cache.test.ts +121 -0
  10. package/src/__tests__/abs-modules-graph.test.ts +100 -0
  11. package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
  12. package/src/__tests__/analyzer.test.ts +745 -2
  13. package/src/__tests__/as-replace.test.ts +165 -0
  14. package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
  15. package/src/__tests__/bpath-as.test.ts +54 -0
  16. package/src/__tests__/bpath-cache-key.test.ts +33 -0
  17. package/src/__tests__/bpath-calls.test.ts +55 -0
  18. package/src/__tests__/bpath-diagnostics.test.ts +78 -0
  19. package/src/__tests__/bpath-env-modules.test.ts +30 -0
  20. package/src/__tests__/bpath-env.test.ts +80 -0
  21. package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
  22. package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
  23. package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
  24. package/src/__tests__/bpath-method-diag.test.ts +45 -0
  25. package/src/__tests__/bpath-module-diags.test.ts +101 -0
  26. package/src/__tests__/bpath-path-env.test.ts +46 -0
  27. package/src/__tests__/bpath-reexport.test.ts +131 -0
  28. package/src/__tests__/bpath-replace.test.ts +52 -0
  29. package/src/__tests__/bpath-require.test.ts +66 -0
  30. package/src/__tests__/bpath-run.test.ts +109 -0
  31. package/src/__tests__/case-emitter.test.ts +636 -0
  32. package/src/__tests__/cjs-require.test.ts +54 -0
  33. package/src/__tests__/diagnostics.test.ts +60 -0
  34. package/src/__tests__/dts-generator.test.ts +290 -6
  35. package/src/__tests__/fn-cache-evict.test.ts +56 -0
  36. package/src/__tests__/fn-sig-impl.test.ts +247 -0
  37. package/src/__tests__/guard-generator.test.ts +29 -0
  38. package/src/__tests__/harvest-auto.test.ts +43 -0
  39. package/src/__tests__/harvest-node.test.ts +24 -0
  40. package/src/__tests__/harvest-package.test.ts +38 -0
  41. package/src/__tests__/harvest-to-abs.test.ts +46 -0
  42. package/src/__tests__/hover-intension.test.ts +39 -0
  43. package/src/__tests__/infer-json.test.ts +35 -0
  44. package/src/__tests__/integration.test.ts +421 -0
  45. package/src/__tests__/m3-intension.test.ts +28 -0
  46. package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
  47. package/src/__tests__/schema-generator.test.ts +38 -0
  48. package/src/__tests__/session-cache-evict.test.ts +90 -0
  49. package/src/__tests__/static-imports.test.ts +44 -0
  50. package/src/abs-modules-graph.ts +387 -0
  51. package/src/analysis-file-cache.ts +51 -0
  52. package/src/analyzer.ts +2569 -94
  53. package/src/bpath-diagnostics.ts +283 -0
  54. package/src/bpath-run.ts +409 -0
  55. package/src/case-emitter.ts +477 -0
  56. package/src/dts-generator.ts +220 -24
  57. package/src/env-to-abs.ts +57 -0
  58. package/src/fn-analysis-cache.ts +119 -0
  59. package/src/guard-generator.ts +57 -0
  60. package/src/harvest-auto.ts +86 -0
  61. package/src/harvest-node.ts +48 -0
  62. package/src/harvest-package.ts +204 -0
  63. package/src/harvest-to-abs.ts +129 -0
  64. package/src/index.ts +128 -0
  65. package/src/infer-json.ts +137 -0
  66. package/src/load-module.ts +34 -0
  67. package/src/mock-abs.ts +275 -0
  68. package/src/schema-generator.ts +43 -0
  69. package/src/semantic-tokens.ts +216 -0
  70. package/src/session-cache.ts +50 -0
  71. package/src/static-imports.ts +241 -0
  72. package/src/target-path.ts +17 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * B 路径静态诊断(AST,不执行):
3
+ * - unreachable:同块 return/throw 之后的语句
4
+ * - builtin-unknown:未声明的全局调用(非 import / 局部 / $runtime)
5
+ */
6
+
7
+ import { parseSource } from "@nudojs/core";
8
+ import type { File, Node, Statement, Identifier } from "@babel/types";
9
+
10
+ export type BPathLoc = {
11
+ start: { line: number; column: number };
12
+ end: { line: number; column: number };
13
+ };
14
+
15
+ export type BPathUnreachable = { range: BPathLoc };
16
+ export type BPathBuiltinUnknown = { name: string; range: BPathLoc };
17
+
18
+ export type BPathDiagnostics = {
19
+ unreachable: BPathUnreachable[];
20
+ builtinUnknown: BPathBuiltinUnknown[];
21
+ };
22
+
23
+ function locOf(node: Node): BPathLoc | null {
24
+ if (!node.loc) return null;
25
+ return {
26
+ start: { line: node.loc.start.line, column: node.loc.start.column },
27
+ end: { line: node.loc.end.line, column: node.loc.end.column },
28
+ };
29
+ }
30
+
31
+ function isExit(stmt: Statement): boolean {
32
+ return stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement";
33
+ }
34
+
35
+ function walkUnreachable(node: Node, out: BPathUnreachable[]): void {
36
+ const n = node as {
37
+ type: string;
38
+ body?: unknown;
39
+ consequent?: Node;
40
+ alternate?: Node | null | undefined;
41
+ block?: Node;
42
+ handler?: { body?: Node } | null;
43
+ finalizer?: Node | null;
44
+ [k: string]: unknown;
45
+ };
46
+
47
+ const checkBlock = (block: unknown) => {
48
+ if (!block) return;
49
+ const body =
50
+ (block as { body?: Statement[] }).body ??
51
+ (Array.isArray(block) ? (block as Statement[]) : null);
52
+ if (!Array.isArray(body)) return;
53
+ for (let i = 0; i < body.length; i++) {
54
+ walkUnreachable(body[i]!, out);
55
+ if (isExit(body[i]!)) {
56
+ for (let j = i + 1; j < body.length; j++) {
57
+ const r = locOf(body[j]!);
58
+ if (r) out.push({ range: r });
59
+ }
60
+ break;
61
+ }
62
+ }
63
+ };
64
+
65
+ switch (n.type) {
66
+ case "Program":
67
+ case "BlockStatement":
68
+ checkBlock(n.body);
69
+ break;
70
+ case "FunctionDeclaration":
71
+ case "FunctionExpression":
72
+ case "ArrowFunctionExpression":
73
+ if (n.body) walkUnreachable(n.body as Node, out);
74
+ break;
75
+ case "IfStatement":
76
+ if (n.consequent) walkUnreachable(n.consequent, out);
77
+ if (n.alternate) walkUnreachable(n.alternate, out);
78
+ break;
79
+ case "ForStatement":
80
+ case "ForInStatement":
81
+ case "ForOfStatement":
82
+ case "WhileStatement":
83
+ case "DoWhileStatement":
84
+ if (n.body) walkUnreachable(n.body as Node, out);
85
+ break;
86
+ case "TryStatement":
87
+ if (n.block) walkUnreachable(n.block as Node, out);
88
+ if (n.handler?.body) walkUnreachable(n.handler.body, out);
89
+ if (n.finalizer) walkUnreachable(n.finalizer as Node, out);
90
+ break;
91
+ case "SwitchStatement":
92
+ for (const c of (n as { cases?: Array<{ consequent?: Statement[] }> }).cases ?? []) {
93
+ checkBlock(c.consequent);
94
+ }
95
+ break;
96
+ case "ExportNamedDeclaration":
97
+ case "ExportDefaultDeclaration": {
98
+ const decl = (n as { declaration?: Node }).declaration;
99
+ if (decl) walkUnreachable(decl, out);
100
+ break;
101
+ }
102
+ default:
103
+ break;
104
+ }
105
+ }
106
+
107
+ /** 文件内声明的名字(函数/类/变量/参数/import) */
108
+ function collectDeclared(file: File): Set<string> {
109
+ const names = new Set<string>();
110
+ const addId = (id: Node | null | undefined) => {
111
+ if (!id) return;
112
+ if (id.type === "Identifier") names.add((id as Identifier).name);
113
+ if (id.type === "ObjectPattern") {
114
+ for (const p of (id as { properties: Array<{ value?: Node }> }).properties) {
115
+ addId(p.value);
116
+ }
117
+ }
118
+ if (id.type === "ArrayPattern") {
119
+ for (const el of (id as { elements: Array<Node | null> }).elements) addId(el);
120
+ }
121
+ };
122
+ const visit = (node: unknown) => {
123
+ if (!node || typeof node !== "object") return;
124
+ const o = node as { type?: string; [k: string]: unknown };
125
+ switch (o.type) {
126
+ case "FunctionDeclaration":
127
+ case "ClassDeclaration":
128
+ addId((o as { id?: Node }).id);
129
+ break;
130
+ case "VariableDeclarator":
131
+ addId((o as { id?: Node }).id);
132
+ break;
133
+ case "ImportSpecifier":
134
+ case "ImportDefaultSpecifier":
135
+ case "ImportNamespaceSpecifier":
136
+ addId((o as { local?: Node }).local);
137
+ break;
138
+ default:
139
+ break;
140
+ }
141
+ // params
142
+ if (o.params && Array.isArray(o.params)) {
143
+ for (const p of o.params) addId(p as Node);
144
+ }
145
+ for (const key of Object.keys(o)) {
146
+ if (key === "loc" || key === "start" || key === "end") continue;
147
+ const v = o[key];
148
+ if (Array.isArray(v)) v.forEach(visit);
149
+ else if (v && typeof v === "object") visit(v);
150
+ }
151
+ };
152
+ visit(file);
153
+ return names;
154
+ }
155
+
156
+ /** 已知可接受的全局(不报 builtin-unknown)——不含 WeakRef 等未覆盖 API */
157
+ const KNOWN_GLOBALS = new Set([
158
+ "Object",
159
+ "Array",
160
+ "String",
161
+ "Number",
162
+ "Boolean",
163
+ "Symbol",
164
+ "Math",
165
+ "JSON",
166
+ "Promise",
167
+ "Error",
168
+ "TypeError",
169
+ "RangeError",
170
+ "SyntaxError",
171
+ "RegExp",
172
+ "Date",
173
+ "Map",
174
+ "Set",
175
+ "WeakMap",
176
+ "WeakSet",
177
+ "console",
178
+ "undefined",
179
+ "NaN",
180
+ "Infinity",
181
+ "globalThis",
182
+ "parseInt",
183
+ "parseFloat",
184
+ "isNaN",
185
+ "isFinite",
186
+ ]);
187
+
188
+ function isFreeUnknown(
189
+ name: string,
190
+ declared: Set<string>,
191
+ seen: Set<string>,
192
+ known: Set<string>,
193
+ ): boolean {
194
+ return (
195
+ !declared.has(name) &&
196
+ !known.has(name) &&
197
+ !name.startsWith("$") &&
198
+ !seen.has(name)
199
+ );
200
+ }
201
+
202
+ function walkBuiltinUnknown(
203
+ node: Node,
204
+ declared: Set<string>,
205
+ out: BPathBuiltinUnknown[],
206
+ seen: Set<string>,
207
+ known: Set<string>,
208
+ parentKey?: string,
209
+ ): void {
210
+ const n = node as {
211
+ type?: string;
212
+ callee?: Node;
213
+ name?: string;
214
+ [k: string]: unknown;
215
+ };
216
+ const flag = (idNode: Node, name: string) => {
217
+ if (!isFreeUnknown(name, declared, seen, known)) return;
218
+ const range = locOf(idNode) ?? locOf(node);
219
+ if (range) {
220
+ out.push({ name, range });
221
+ seen.add(name);
222
+ }
223
+ };
224
+
225
+ if (n.type === "CallExpression" && n.callee?.type === "Identifier") {
226
+ flag(n.callee, (n.callee as Identifier).name);
227
+ } else if (
228
+ n.type === "Identifier" &&
229
+ typeof n.name === "string" &&
230
+ parentKey !== "key" &&
231
+ parentKey !== "property" &&
232
+ parentKey !== "local" &&
233
+ parentKey !== "id"
234
+ ) {
235
+ // 裸标识符引用(如 return WeakRef)
236
+ flag(node as Node, n.name);
237
+ }
238
+
239
+ for (const key of Object.keys(n)) {
240
+ if (key === "loc" || key === "start" || key === "end") continue;
241
+ const v = n[key];
242
+ if (Array.isArray(v)) {
243
+ v.forEach((x) => {
244
+ if (x && typeof x === "object" && "type" in (x as object)) {
245
+ walkBuiltinUnknown(x as Node, declared, out, seen, known, key);
246
+ }
247
+ });
248
+ } else if (v && typeof v === "object" && "type" in (v as object)) {
249
+ walkBuiltinUnknown(v as Node, declared, out, seen, known, key);
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 静态收集 B 路径诊断。
256
+ * extraKnown:@nudo:mock / @nudo:env 已覆盖的全局名(B 注入后不再是裸原生
257
+ * 调用,不得误报 builtin-unknown——收集器只吃 AST,看不到指令)。
258
+ */
259
+ export function collectBPathDiagnostics(
260
+ source: string,
261
+ extraKnown?: Iterable<string>,
262
+ ): BPathDiagnostics {
263
+ let file: File;
264
+ try {
265
+ file = parseSource(source);
266
+ } catch {
267
+ return { unreachable: [], builtinUnknown: [] };
268
+ }
269
+ const unreachable: BPathUnreachable[] = [];
270
+ // File → program
271
+ const root = (file as unknown as { type: string; program?: Node }).program ?? file;
272
+ walkUnreachable(root as Node, unreachable);
273
+
274
+ const declared = collectDeclared(file);
275
+ const known = new Set(KNOWN_GLOBALS);
276
+ if (extraKnown) {
277
+ for (const n of extraKnown) known.add(n);
278
+ }
279
+ const builtinUnknown: BPathBuiltinUnknown[] = [];
280
+ walkBuiltinUnknown(file, declared, builtinUnknown, new Set(), known);
281
+
282
+ return { unreachable, builtinUnknown };
283
+ }
@@ -0,0 +1,409 @@
1
+ /**
2
+ * B 路径能力判定 + 进程内 transpile 执行(service 入口)。
3
+ *
4
+ * 分析默认 mode="analyze":不执行顶层副作用,只保留函数定义。
5
+ * throws 经 callTranspiledExportFull 捕获 $throw。
6
+ */
7
+
8
+ import {
9
+ runTranspiled,
10
+ callTranspiledExport,
11
+ callTranspiledExportFull,
12
+ setBCallCollector,
13
+ setMemberDiagCollector,
14
+ setAbsTruncationCollector,
15
+ typeValueToAbs,
16
+ createEnvironment,
17
+ type BCallRecord,
18
+ type BMemberDiag,
19
+ type TranspiledCallResult,
20
+ type Abs,
21
+ type AbsModuleExports,
22
+ stableAnalyzeKeySource,
23
+ formatAbs,
24
+ hashSource,
25
+ getFnImpl,
26
+ } from "@nudojs/core";
27
+ import { parse, extractInlineDirectives } from "@nudojs/parser";
28
+ import { loadEnvs } from "@nudojs/cli/evaluator";
29
+ import { evalAbsModuleGraph } from "./abs-modules-graph.ts";
30
+ import { envValueToAbs } from "./env-to-abs.ts";
31
+ import { clearAnalysisFileCache } from "./analysis-file-cache.ts";
32
+ import { clearFnAnalysisCache } from "./fn-analysis-cache.ts";
33
+
34
+ /** Content part for one Abs mock seed. */
35
+ function absSeedPart(a: Abs): string {
36
+ const impl = getFnImpl(a);
37
+ if (impl?.fingerprint) return impl.fingerprint;
38
+ if (a.shape.k === "fn") {
39
+ const ret = impl?.env?.vars?.get("__nudo_mock_ret");
40
+ if (ret) {
41
+ try {
42
+ return `ret=${formatAbs(ret)}`;
43
+ } catch {
44
+ return "ret=?";
45
+ }
46
+ }
47
+ if (impl?.apply) return "apply";
48
+ }
49
+ try {
50
+ return formatAbs(a);
51
+ } catch {
52
+ return "?";
53
+ }
54
+ }
55
+
56
+ function seedFnPart(name: string, fn: { params: string[]; body: unknown; fingerprint?: string }): string {
57
+ if (fn.fingerprint) return `${name}:${fn.fingerprint}`;
58
+ const body = fn.body as { start?: number; end?: number; type?: string } | undefined;
59
+ const bodyKey =
60
+ body && typeof body.start === "number" && typeof body.end === "number"
61
+ ? `${body.start}:${body.end}:${body.type ?? ""}`
62
+ : String(body?.type ?? "?");
63
+ return `${name}(${fn.params.join(",")})@${bodyKey}`;
64
+ }
65
+
66
+ /**
67
+ * Cache key for @nudo:mock seeds: name list alone is not enough — same names
68
+ * with different Abs values must miss. Uses AbsFnImpl.fingerprint when the
69
+ * mock pipeline stamped one (formatAbs cannot see WeakMap-side returns/withArgs).
70
+ */
71
+ export function mockSeedFingerprint(
72
+ mocks?: Record<string, Abs>,
73
+ seedFns?: Record<string, { params: string[]; body: unknown; fingerprint?: string }>,
74
+ ): string {
75
+ const parts: string[] = [];
76
+ if (mocks) {
77
+ const names = Object.keys(mocks).sort();
78
+ for (const n of names) parts.push(`${n}=${absSeedPart(mocks[n]!)}`);
79
+ }
80
+ if (seedFns) {
81
+ const names = Object.keys(seedFns).sort();
82
+ for (const n of names) parts.push(`fn:${seedFnPart(n, seedFns[n]!)}`);
83
+ }
84
+ if (parts.length === 0) return "-";
85
+ return hashSource(parts.join(";"));
86
+ }
87
+
88
+ /** @nudo:env → Abs 全局表(保留 fnSig impl) */
89
+ export function collectEnvGlobals(envNames: string[]): Record<string, Abs> {
90
+ if (envNames.length === 0) return {};
91
+ const env = createEnvironment();
92
+ try {
93
+ loadEnvs(envNames, env);
94
+ } catch {
95
+ return {};
96
+ }
97
+ const out: Record<string, Abs> = {};
98
+ for (const [k, v] of Object.entries(env.getOwnBindings())) {
99
+ out[k] = envValueToAbs(v);
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** @nudo:env modules(path / node:path / fs…)→ AbsModuleExports */
105
+ export function collectEnvModules(envNames: string[]): Record<string, AbsModuleExports> {
106
+ if (envNames.length === 0) return {};
107
+ const env = createEnvironment();
108
+ let mods: Record<string, Record<string, import("@nudojs/core").TypeValue>> = {};
109
+ try {
110
+ mods = loadEnvs(envNames, env).modules;
111
+ } catch {
112
+ return {};
113
+ }
114
+ const out: Record<string, AbsModuleExports> = {};
115
+ for (const [spec, exports] of Object.entries(mods)) {
116
+ const named: Record<string, Abs> = {};
117
+ for (const [k, v] of Object.entries(exports)) {
118
+ named[k] = envValueToAbs(v);
119
+ }
120
+ out[spec] = { named };
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /** 收集 @nudo:replace + @nudo:as → transpile 注入表 */
126
+ export function collectBPathReplacements(source: string): {
127
+ targets: Array<{
128
+ target: string;
129
+ varName: string;
130
+ stmtStart?: number;
131
+ stmtEnd?: number;
132
+ }>;
133
+ values: Record<string, Abs>;
134
+ asTargets: Array<{ varName: string; stmtStart: number; stmtEnd: number }>;
135
+ asValues: Record<string, Abs>;
136
+ } {
137
+ const targets: Array<{
138
+ target: string;
139
+ varName: string;
140
+ stmtStart?: number;
141
+ stmtEnd?: number;
142
+ }> = [];
143
+ const values: Record<string, Abs> = {};
144
+ const asTargets: Array<{ varName: string; stmtStart: number; stmtEnd: number }> = [];
145
+ const asValues: Record<string, Abs> = {};
146
+ let i = 0;
147
+ try {
148
+ const file = parse(source);
149
+ const visitStmts = (stmts: unknown[]) => {
150
+ for (const stmt of stmts) {
151
+ if (!stmt || typeof stmt !== "object") continue;
152
+ const loc = (stmt as { loc?: { start: { line: number }; end: { line: number } } }).loc;
153
+ const dirs = extractInlineDirectives(stmt as never);
154
+ for (const d of dirs) {
155
+ if (d.kind === "replace") {
156
+ const varName = `__rep${i++}`;
157
+ targets.push({
158
+ target: d.targetSource,
159
+ varName,
160
+ stmtStart: loc?.start.line,
161
+ stmtEnd: loc?.end.line,
162
+ });
163
+ values[varName] = typeValueToAbs(d.typeExpr);
164
+ } else if (d.kind === "as" && loc) {
165
+ const varName = `__as${i++}`;
166
+ asTargets.push({
167
+ varName,
168
+ stmtStart: loc.start.line,
169
+ stmtEnd: loc.end.line,
170
+ });
171
+ asValues[varName] = typeValueToAbs(d.typeExpr);
172
+ }
173
+ }
174
+ const s = stmt as {
175
+ type?: string;
176
+ body?: unknown;
177
+ block?: unknown;
178
+ consequent?: unknown;
179
+ alternate?: unknown;
180
+ declaration?: unknown;
181
+ };
182
+ if (s.type === "BlockStatement" && Array.isArray(s.body)) visitStmts(s.body);
183
+ if (s.type === "FunctionDeclaration" || s.type === "FunctionExpression") {
184
+ if (s.body) visitStmts([s.body]);
185
+ }
186
+ if (s.type === "ExportNamedDeclaration" && s.declaration) {
187
+ visitStmts([s.declaration]);
188
+ }
189
+ if (s.type === "IfStatement") {
190
+ if (s.consequent) visitStmts([s.consequent]);
191
+ if (s.alternate) visitStmts([s.alternate]);
192
+ }
193
+ }
194
+ };
195
+ visitStmts(file.program.body);
196
+ } catch {
197
+ /* ignore */
198
+ }
199
+ return { targets, values, asTargets, asValues };
200
+ }
201
+
202
+ /** 可走 transpile+exec:env 经 loadEnvs(内置 + 已 preload 的路径型) */
203
+ export function isBPathCapable(source: string, envNames: string[] = []): boolean {
204
+ void envNames;
205
+ // 顶层 this. 仍不支持(方法内 this 由 transpile 处理)
206
+ if (/(^|[^.\w$])this\s*\./.test(source) && !/\bclass\s+/.test(source)) return false;
207
+ return true;
208
+ }
209
+
210
+ export type BPathRunResult = {
211
+ exports: Record<string, unknown>;
212
+ modules: Record<string, AbsModuleExports>;
213
+ /** 顶层执行期 method-missing */
214
+ memberDiags?: BMemberDiag[];
215
+ /** 模块图 cycle/depth/missing(B 权威) */
216
+ moduleIssues?: import("./abs-modules-graph.ts").AbsModuleLoadIssue[];
217
+ /** Abs 求值递归截断的函数标签 */
218
+ truncatedFns?: string[];
219
+ /** 顶层 $callNamed 调用点(call@ 合成;不经 TypeValue collector) */
220
+ calls?: BCallRecord[];
221
+ };
222
+
223
+ /** 按入口文件键控:同文件同源 O(1) 身份比较(case 循环 400 次不再重哈希) */
224
+ type BCacheEntry = {
225
+ stableSource: string;
226
+ mode: string;
227
+ envKey: string;
228
+ mockKey: string;
229
+ value: BPathRunResult | null;
230
+ };
231
+ const bRunByFile = new Map<string, BCacheEntry>();
232
+ const MAX_B_RUN_CACHE = 32;
233
+
234
+ export function clearBPathCache(): void {
235
+ bRunByFile.clear();
236
+ // 测试/宿主习惯:清 B-path 时一并丢掉整文件/函数级分析缓存
237
+ clearAnalysisFileCache();
238
+ clearFnAnalysisCache();
239
+ }
240
+
241
+ /** 依赖文件变更后:逐出以这些文件为入口的 B-path 缓存 */
242
+ export function evictBPathCacheForFiles(files: string[]): number {
243
+ let n = 0;
244
+ for (const f of files) {
245
+ if (bRunByFile.delete(f)) n++;
246
+ }
247
+ return n;
248
+ }
249
+
250
+ function bPathCacheSet(
251
+ filePath: string,
252
+ stableSource: string,
253
+ mode: string,
254
+ envKey: string,
255
+ mockKey: string,
256
+ value: BPathRunResult | null,
257
+ ): void {
258
+ if (bRunByFile.size >= MAX_B_RUN_CACHE && !bRunByFile.has(filePath)) {
259
+ const oldest = bRunByFile.keys().next().value;
260
+ if (oldest !== undefined) bRunByFile.delete(oldest);
261
+ }
262
+ bRunByFile.set(filePath, { stableSource, mode, envKey, mockKey, value });
263
+ }
264
+
265
+ /** 模块图 + runTranspiled(默认 analyze 模式) */
266
+ export function tryRunBPath(
267
+ source: string,
268
+ filePath: string,
269
+ opts: {
270
+ maxLoopIters?: number;
271
+ mode?: "exec" | "analyze";
272
+ envNames?: string[];
273
+ /** @nudo:mock → Abs,注入为全局绑定(防止顶层调用真 fetch 等) */
274
+ mocks?: Record<string, Abs>;
275
+ } = {},
276
+ ): BPathRunResult | undefined {
277
+ if (!isBPathCapable(source, opts.envNames ?? [])) return undefined;
278
+ const mode = opts.mode ?? "analyze";
279
+ const envKey = (opts.envNames ?? []).join(",");
280
+ const mockKey = mockSeedFingerprint(opts.mocks);
281
+ // 尾部无 @nudo 注释不参与:comment-only 编辑命中 B-path。
282
+ // 同 source 引用时 stable 快路径返回原串 → 下方 === 为 O(1)。
283
+ const stable = stableAnalyzeKeySource(source);
284
+ const cached = bRunByFile.get(filePath);
285
+ if (
286
+ cached &&
287
+ cached.stableSource === stable &&
288
+ cached.mode === mode &&
289
+ cached.envKey === envKey &&
290
+ cached.mockKey === mockKey
291
+ ) {
292
+ // LRU:命中移到队尾
293
+ bRunByFile.delete(filePath);
294
+ bRunByFile.set(filePath, cached);
295
+ return cached.value ?? undefined;
296
+ }
297
+ let out: BPathRunResult | null = null;
298
+ try {
299
+ const memberDiags: BMemberDiag[] = [];
300
+ const truncated = new Set<string>();
301
+ const topCalls: BCallRecord[] = [];
302
+ // collector 先于模块图:import 函数体在 evalProgramAbs 内的 method-missing 也要收
303
+ setMemberDiagCollector((d) => memberDiags.push(d));
304
+ setAbsTruncationCollector((label) => truncated.add(label));
305
+ setBCallCollector((r) => topCalls.push(r));
306
+ try {
307
+ const { modules: graphMods, issues } = evalAbsModuleGraph(source, filePath);
308
+ const envMods = collectEnvModules(opts.envNames ?? []);
309
+ const modules = { ...envMods, ...graphMods };
310
+ const { targets, values, asTargets, asValues } = collectBPathReplacements(source);
311
+ const envGlobals = {
312
+ ...collectEnvGlobals(opts.envNames ?? []),
313
+ ...(opts.mocks ?? {}),
314
+ };
315
+ const exports = runTranspiled(source, {
316
+ modules: modules as never,
317
+ maxLoopIters: opts.maxLoopIters,
318
+ mode: opts.mode ?? "analyze",
319
+ replacementTargets: targets.length ? targets : undefined,
320
+ replacements: targets.length ? values : undefined,
321
+ asOverrideTargets: asTargets.length ? asTargets : undefined,
322
+ asOverrides: asTargets.length ? asValues : undefined,
323
+ envGlobals: Object.keys(envGlobals).length ? envGlobals : undefined,
324
+ });
325
+ out = {
326
+ exports,
327
+ modules,
328
+ memberDiags: memberDiags.length ? memberDiags : undefined,
329
+ moduleIssues: issues.length ? issues : undefined,
330
+ truncatedFns: truncated.size ? [...truncated] : undefined,
331
+ calls: topCalls.length ? topCalls : undefined,
332
+ };
333
+ } finally {
334
+ setMemberDiagCollector(null);
335
+ setAbsTruncationCollector(null);
336
+ setBCallCollector(null);
337
+ }
338
+ } catch {
339
+ out = null;
340
+ }
341
+ bPathCacheSet(filePath, stable, mode, envKey, mockKey, out);
342
+ return out ?? undefined;
343
+ }
344
+
345
+ /** B 路径求值具名导出(结果 + throws);opts.collectCalls 时附带调用点记录 */
346
+ export function tryBPathCallFull(
347
+ source: string,
348
+ filePath: string,
349
+ fnName: string,
350
+ args: Abs[],
351
+ opts: {
352
+ collectCalls?: boolean;
353
+ collectMemberDiags?: boolean;
354
+ envNames?: string[];
355
+ mocks?: Record<string, Abs>;
356
+ } = {},
357
+ ): (TranspiledCallResult & {
358
+ calls?: BCallRecord[];
359
+ memberDiags?: BMemberDiag[];
360
+ moduleIssues?: import("./abs-modules-graph.ts").AbsModuleLoadIssue[];
361
+ truncatedFns?: string[];
362
+ }) | undefined {
363
+ const run = tryRunBPath(source, filePath, { envNames: opts.envNames, mocks: opts.mocks });
364
+ if (!run) return undefined;
365
+ if (!(fnName in run.exports)) return undefined;
366
+ const collected: BCallRecord[] = [...(run.calls ?? [])];
367
+ const memberDiags: BMemberDiag[] = [];
368
+ if (opts.collectCalls) {
369
+ setBCallCollector((r) => collected.push(r));
370
+ }
371
+ if (opts.collectMemberDiags ?? true) {
372
+ setMemberDiagCollector((d) => memberDiags.push(d));
373
+ }
374
+ try {
375
+ const full = callTranspiledExportFull(run.exports, fnName, args);
376
+ const all = [...(run.memberDiags ?? []), ...memberDiags];
377
+ return {
378
+ ...full,
379
+ calls: opts.collectCalls || run.calls?.length ? collected : undefined,
380
+ memberDiags: all.length ? all : undefined,
381
+ moduleIssues: run.moduleIssues,
382
+ truncatedFns: run.truncatedFns,
383
+ };
384
+ } finally {
385
+ if (opts.collectCalls) setBCallCollector(null);
386
+ setMemberDiagCollector(null);
387
+ }
388
+ }
389
+
390
+ /** B 路径求值具名导出(仅成功结果) */
391
+ export function tryBPathCall(
392
+ source: string,
393
+ filePath: string,
394
+ fnName: string,
395
+ args: Abs[],
396
+ opts: { envNames?: string[]; mocks?: Record<string, Abs> } = {},
397
+ ): Abs | undefined {
398
+ const full = tryBPathCallFull(source, filePath, fnName, args, opts);
399
+ if (!full) return undefined;
400
+ const r = full.result;
401
+ if (!r) return undefined;
402
+ if (r.shape.k === "never" && full.throws.shape.k !== "never") {
403
+ return undefined;
404
+ }
405
+ if (r.shape.k === "unknown" && !r.term) return undefined;
406
+ return r;
407
+ }
408
+
409
+ export { callTranspiledExport, callTranspiledExportFull };