@lingfan-engine/stories-build 0.0.0-stage → 0.1.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 (110) hide show
  1. package/LICENSE +201 -0
  2. package/bin/stories-build.mjs +16 -0
  3. package/bin/stories-watch.mjs +16 -0
  4. package/package.json +50 -3
  5. package/src/authoring.d.ts +51 -0
  6. package/src/cell/errors.ts +12 -0
  7. package/src/cell/imports.ts +127 -0
  8. package/src/cell/index.ts +33 -0
  9. package/src/cell/render-register.ts +33 -0
  10. package/src/cell/scan-cells.ts +63 -0
  11. package/src/cell/scan-file.ts +108 -0
  12. package/src/cell/types.ts +27 -0
  13. package/src/cell/whitelist.ts +22 -0
  14. package/src/cli/cli.ts +99 -0
  15. package/src/cli/watch-cli.ts +42 -0
  16. package/src/compiler/cell-scan.ts +27 -0
  17. package/src/compiler/compiler-contract.ts +87 -0
  18. package/src/compiler/extension-load.ts +67 -0
  19. package/src/compiler/index.ts +22 -0
  20. package/src/compiler/story-compiler/assemble/functions.ts +202 -0
  21. package/src/compiler/story-compiler/assemble/index.ts +8 -0
  22. package/src/compiler/story-compiler/assemble/session.ts +207 -0
  23. package/src/compiler/story-compiler/assemble/types.ts +37 -0
  24. package/src/compiler/story-compiler/assemble.ts +5 -0
  25. package/src/compiler/story-compiler/graph/build.ts +208 -0
  26. package/src/compiler/story-compiler/graph/core.ts +102 -0
  27. package/src/compiler/story-compiler/graph/fold.ts +121 -0
  28. package/src/compiler/story-compiler/graph/index.ts +17 -0
  29. package/src/compiler/story-compiler/graph/scan/bindings.ts +277 -0
  30. package/src/compiler/story-compiler/graph/scan/imports.ts +170 -0
  31. package/src/compiler/story-compiler/graph/scan/index.ts +9 -0
  32. package/src/compiler/story-compiler/graph/scan/module-scan.ts +199 -0
  33. package/src/compiler/story-compiler/graph/state-root.ts +147 -0
  34. package/src/compiler/story-compiler/graph.ts +18 -0
  35. package/src/compiler/story-compiler/index.ts +34 -0
  36. package/src/compiler/story-compiler/translate/expr/assign.ts +204 -0
  37. package/src/compiler/story-compiler/translate/expr/engine-text.ts +16 -0
  38. package/src/compiler/story-compiler/translate/expr/expr.ts +174 -0
  39. package/src/compiler/story-compiler/translate/expr/index.ts +24 -0
  40. package/src/compiler/story-compiler/translate/expr/lower/index.ts +4 -0
  41. package/src/compiler/story-compiler/translate/expr/lower/lower.ts +266 -0
  42. package/src/compiler/story-compiler/translate/expr/lower/numeric.ts +56 -0
  43. package/src/compiler/story-compiler/translate/expr/probe.ts +23 -0
  44. package/src/compiler/story-compiler/translate/expr/state-path.ts +47 -0
  45. package/src/compiler/story-compiler/translate/expr/words/arg.ts +87 -0
  46. package/src/compiler/story-compiler/translate/expr/words/call.ts +125 -0
  47. package/src/compiler/story-compiler/translate/expr/words/callee.ts +43 -0
  48. package/src/compiler/story-compiler/translate/expr/words/command.ts +93 -0
  49. package/src/compiler/story-compiler/translate/expr/words/dispatch.ts +16 -0
  50. package/src/compiler/story-compiler/translate/expr/words/index.ts +8 -0
  51. package/src/compiler/story-compiler/translate/expr/words/static-for.ts +96 -0
  52. package/src/compiler/story-compiler/translate/font-check.ts +94 -0
  53. package/src/compiler/story-compiler/translate/index.ts +10 -0
  54. package/src/compiler/story-compiler/translate/shared.ts +132 -0
  55. package/src/compiler/story-compiler/translate/stmt/control/branch.ts +71 -0
  56. package/src/compiler/story-compiler/translate/stmt/control/index.ts +11 -0
  57. package/src/compiler/story-compiler/translate/stmt/control/iter.ts +72 -0
  58. package/src/compiler/story-compiler/translate/stmt/control/loop.ts +114 -0
  59. package/src/compiler/story-compiler/translate/stmt/control/switch.ts +75 -0
  60. package/src/compiler/story-compiler/translate/stmt/control-flow.ts +25 -0
  61. package/src/compiler/story-compiler/translate/stmt/index.ts +17 -0
  62. package/src/compiler/story-compiler/translate/stmt/statements.ts +145 -0
  63. package/src/compiler/story-compiler/translate.ts +15 -0
  64. package/src/compiler/story-compiler.ts +119 -0
  65. package/src/config/index.ts +15 -0
  66. package/src/config/manifest-read.ts +26 -0
  67. package/src/config/state-type.ts +85 -0
  68. package/src/config/story-config.ts +113 -0
  69. package/src/const-eval/config-eval.ts +181 -0
  70. package/src/const-eval/const-eval-pass.ts +133 -0
  71. package/src/const-eval/const-eval.ts +114 -0
  72. package/src/const-eval/const-expr.ts +358 -0
  73. package/src/const-eval/contracts.ts +31 -0
  74. package/src/const-eval/define-merge.ts +195 -0
  75. package/src/const-eval/diag/compare.ts +39 -0
  76. package/src/const-eval/diag/index.ts +5 -0
  77. package/src/const-eval/diag/locate.ts +45 -0
  78. package/src/const-eval/index.ts +44 -0
  79. package/src/const-eval/module-characters.ts +353 -0
  80. package/src/const-eval/module-define.ts +300 -0
  81. package/src/const-eval/round/evaluate.ts +149 -0
  82. package/src/const-eval/round/index.ts +4 -0
  83. package/src/const-eval/round/source.ts +134 -0
  84. package/src/const-eval/state-schema.ts +269 -0
  85. package/src/discovery/index.ts +7 -0
  86. package/src/discovery/source-discovery.ts +193 -0
  87. package/src/emit/compile-roundtrip.ts +45 -0
  88. package/src/emit/emit.ts +147 -0
  89. package/src/emit/index.ts +9 -0
  90. package/src/emit/report.ts +29 -0
  91. package/src/fonts/font-registry.ts +53 -0
  92. package/src/fonts/index.ts +7 -0
  93. package/src/index.ts +76 -0
  94. package/src/pipeline/gates.ts +107 -0
  95. package/src/pipeline/index.ts +18 -0
  96. package/src/pipeline/orchestrate.ts +459 -0
  97. package/src/pipeline/watch.ts +244 -0
  98. package/src/shared/errors.ts +2 -0
  99. package/src/shared/index.ts +11 -0
  100. package/src/shared/texts.ts +85 -0
  101. package/src/shared/walk-files.ts +18 -0
  102. package/src/typegen/collect.ts +107 -0
  103. package/src/typegen/font-render.ts +30 -0
  104. package/src/typegen/index.ts +18 -0
  105. package/src/typegen/render.ts +68 -0
  106. package/src/typegen/script-render.ts +21 -0
  107. package/src/typegen/state-enum-ast.ts +365 -0
  108. package/src/typegen/state-render.ts +242 -0
  109. package/src/typegen/types.ts +21 -0
  110. package/README.md +0 -3
@@ -0,0 +1,358 @@
1
+ /**
2
+ * 模块层构建期 define 的常量求值器(AST 侧,零 esbuild)。
3
+ *
4
+ * config.ts 的 define 走 esbuild 求值通道(const-eval,esbuild 求值语义保证常量
5
+ * 折叠与标识符消解);模块层的 define 只发生在顶层语句,值域刻意收窄为
6
+ * 「字面量 / 数组 / 对象 / 枚举成员引用」,本文件用 TS AST 直接折叠,不引入第二套
7
+ * 打包器。枚举值是唯一的非常量字面量形态:标识符按本文件 import 与本地 enum 解析,
8
+ * 被引用的枚举文件再按同文法折叠成员表(只认声明在该文件内的 enum,不追 re-export)。
9
+ *
10
+ * fail-closed:任何不可折叠形态(模板插值、展开、简写、计算属性、npm 枚举、
11
+ * 引用型枚举初始化……)都返回可读原因,由收集器统一挂源文件:行:列定位。
12
+ */
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join, posix } from "node:path";
15
+ import * as ts from "typescript";
16
+ import type { JsonValue } from "./const-eval.ts";
17
+
18
+ /** 一个枚举的成员值表(声明序;值 = 字符串或数字) */
19
+ type EnumTable = ReadonlyMap<string, string | number>;
20
+
21
+ /** 模块 import 登记:本地名 → 模块说明符与出口名(default 导入 exportedName = "default") */
22
+ interface ImportedEnum {
23
+ readonly specifier: string;
24
+ readonly exportedName: string;
25
+ }
26
+
27
+ /** 求值上下文:当前模块的 import 表、本地 enum 表与跨文件枚举缓存(缓存可变) */
28
+ export interface ConstExprContext {
29
+ /** 源目录绝对路径(枚举文件按它读盘) */
30
+ readonly sourcesDir: string;
31
+ /** 当前模块相对源根的 posix 路径(import 说明符按它的目录归一) */
32
+ readonly rel: string;
33
+ readonly imports: ReadonlyMap<string, ImportedEnum>;
34
+ readonly localEnums: ReadonlyMap<string, EnumTable>;
35
+ /** 已解析枚举文件的缓存:文件路径 → 枚举名 → 成员表 */
36
+ readonly enumFiles: Map<string, Map<string, EnumTable>>;
37
+ }
38
+
39
+ export type ConstExprResult =
40
+ { ok: true; value: JsonValue } | { ok: false; reason: string };
41
+
42
+ /** 折叠一个表达式为 JSON 可序列化常量;任何非常量形态都返回可读原因 */
43
+ export function evalConstExpr(
44
+ expr: ts.Expression,
45
+ ctx: ConstExprContext,
46
+ ): ConstExprResult {
47
+ const unwrapped = unwrapExpr(expr);
48
+ if (
49
+ ts.isStringLiteral(unwrapped) ||
50
+ ts.isNoSubstitutionTemplateLiteral(unwrapped)
51
+ ) {
52
+ return { ok: true, value: unwrapped.text };
53
+ }
54
+ if (ts.isNumericLiteral(unwrapped)) {
55
+ return { ok: true, value: Number(unwrapped.text) };
56
+ }
57
+ if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
58
+ return { ok: true, value: true };
59
+ if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
60
+ return { ok: true, value: false };
61
+ if (unwrapped.kind === ts.SyntaxKind.NullKeyword)
62
+ return { ok: true, value: null };
63
+ if (ts.isPrefixUnaryExpression(unwrapped)) {
64
+ return evalPrefixUnary(unwrapped);
65
+ }
66
+ if (ts.isArrayLiteralExpression(unwrapped)) {
67
+ return evalArray(unwrapped, ctx);
68
+ }
69
+ if (ts.isObjectLiteralExpression(unwrapped)) {
70
+ return evalObject(unwrapped, ctx);
71
+ }
72
+ if (ts.isIdentifier(unwrapped)) {
73
+ return resolveEnumObject(unwrapped.text, ctx);
74
+ }
75
+ if (
76
+ ts.isPropertyAccessExpression(unwrapped) &&
77
+ ts.isIdentifier(unwrapped.expression)
78
+ ) {
79
+ return resolveEnumMember(
80
+ unwrapped.expression.text,
81
+ unwrapped.name.text,
82
+ ctx,
83
+ );
84
+ }
85
+ if (
86
+ ts.isElementAccessExpression(unwrapped) &&
87
+ ts.isIdentifier(unwrapped.expression) &&
88
+ ts.isStringLiteral(unwrapped.argumentExpression)
89
+ ) {
90
+ return resolveEnumMember(
91
+ unwrapped.expression.text,
92
+ unwrapped.argumentExpression.text,
93
+ ctx,
94
+ );
95
+ }
96
+ if (ts.isTemplateExpression(unwrapped)) {
97
+ return {
98
+ ok: false,
99
+ reason: "模板串不支持插值——构建期 define 值请用字符串字面量",
100
+ };
101
+ }
102
+ return {
103
+ ok: false,
104
+ reason:
105
+ "值必须是可序列化常量:字符串/数字/布尔/null、数组或对象字面量、枚举成员引用",
106
+ };
107
+ }
108
+
109
+ /** 解包括号与 as/类型断言包裹(求值语义 = 内层表达式) */
110
+ function unwrapExpr(expr: ts.Expression): ts.Expression {
111
+ if (
112
+ ts.isParenthesizedExpression(expr) ||
113
+ ts.isAsExpression(expr) ||
114
+ ts.isTypeAssertionExpression(expr)
115
+ ) {
116
+ return unwrapExpr(expr.expression);
117
+ }
118
+ return expr;
119
+ }
120
+
121
+ function evalPrefixUnary(expr: ts.PrefixUnaryExpression): ConstExprResult {
122
+ if (
123
+ expr.operator !== ts.SyntaxKind.MinusToken &&
124
+ expr.operator !== ts.SyntaxKind.PlusToken
125
+ ) {
126
+ return { ok: false, reason: "一元运算符不支持构建期折叠(只支持正负号)" };
127
+ }
128
+ const inner = unwrapExpr(expr.operand);
129
+ if (!ts.isNumericLiteral(inner)) {
130
+ return { ok: false, reason: "正负号只能作用于数字字面量" };
131
+ }
132
+ const value = Number(inner.text);
133
+ return {
134
+ ok: true,
135
+ value: expr.operator === ts.SyntaxKind.MinusToken ? -value : value,
136
+ };
137
+ }
138
+
139
+ function evalArray(
140
+ expr: ts.ArrayLiteralExpression,
141
+ ctx: ConstExprContext,
142
+ ): ConstExprResult {
143
+ const out: JsonValue[] = [];
144
+ for (const element of expr.elements) {
145
+ if (ts.isOmittedExpression(element)) {
146
+ return { ok: false, reason: "数组字面量不允许空洞" };
147
+ }
148
+ if (ts.isSpreadElement(element)) {
149
+ return { ok: false, reason: "数组字面量不支持展开元素" };
150
+ }
151
+ const item = evalConstExpr(element, ctx);
152
+ if (!item.ok) return item;
153
+ out.push(item.value);
154
+ }
155
+ return { ok: true, value: out };
156
+ }
157
+
158
+ function evalObject(
159
+ expr: ts.ObjectLiteralExpression,
160
+ ctx: ConstExprContext,
161
+ ): ConstExprResult {
162
+ const out: Record<string, JsonValue> = {};
163
+ for (const prop of expr.properties) {
164
+ if (ts.isPropertyAssignment(prop)) {
165
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
166
+ return { ok: false, reason: "对象字面量不支持计算属性名" };
167
+ }
168
+ const value = evalConstExpr(prop.initializer, ctx);
169
+ if (!value.ok) return value;
170
+ out[prop.name.text] = value.value;
171
+ continue;
172
+ }
173
+ if (ts.isShorthandPropertyAssignment(prop)) {
174
+ return {
175
+ ok: false,
176
+ reason: "对象字面量不支持简写属性——请写显式的 键: 值",
177
+ };
178
+ }
179
+ if (ts.isSpreadAssignment(prop)) {
180
+ return { ok: false, reason: "对象字面量不支持展开属性" };
181
+ }
182
+ return { ok: false, reason: "对象字面量只支持普通键值属性" };
183
+ }
184
+ return { ok: true, value: out };
185
+ }
186
+
187
+ /** 标识符 → 枚举对象(本地 enum 优先,其次 import 来源的枚举文件) */
188
+ function resolveEnumObject(
189
+ name: string,
190
+ ctx: ConstExprContext,
191
+ ): ConstExprResult {
192
+ const local = ctx.localEnums.get(name);
193
+ if (local !== undefined) {
194
+ return { ok: true, value: Object.fromEntries(local) };
195
+ }
196
+ const table = loadImportedEnum(name, ctx);
197
+ if (!table.ok) return table;
198
+ return { ok: true, value: Object.fromEntries(table.value) };
199
+ }
200
+
201
+ /** 枚举成员取值:`Sex.Male` / `Sex["Male"]` */
202
+ function resolveEnumMember(
203
+ objName: string,
204
+ member: string,
205
+ ctx: ConstExprContext,
206
+ ): ConstExprResult {
207
+ const local = ctx.localEnums.get(objName);
208
+ if (local !== undefined) {
209
+ return takeEnumMember(local, objName, member);
210
+ }
211
+ const table = loadImportedEnum(objName, ctx);
212
+ if (!table.ok) return table;
213
+ return takeEnumMember(table.value, objName, member);
214
+ }
215
+
216
+ function takeEnumMember(
217
+ table: EnumTable,
218
+ objName: string,
219
+ member: string,
220
+ ): ConstExprResult {
221
+ const value = table.get(member);
222
+ if (value === undefined) {
223
+ return { ok: false, reason: `枚举 "${objName}" 没有 "${member}" 成员` };
224
+ }
225
+ return { ok: true, value };
226
+ }
227
+
228
+ /** 按模块 import 表解析枚举文件并折叠成员表(解析失败返回可读原因) */
229
+ function loadImportedEnum(
230
+ name: string,
231
+ ctx: ConstExprContext,
232
+ ): { ok: true; value: EnumTable } | { ok: false; reason: string } {
233
+ const imported = ctx.imports.get(name);
234
+ if (imported === undefined) {
235
+ return {
236
+ ok: false,
237
+ reason: `无法识别的标识符 "${name}"——构建期 define 值只支持字面量与枚举成员引用`,
238
+ };
239
+ }
240
+ if (imported.exportedName === "default") {
241
+ return {
242
+ ok: false,
243
+ reason: `枚举 "${name}" 经 default 导入——构建期枚举折叠只支持具名导出的 enum`,
244
+ };
245
+ }
246
+ if (!imported.specifier.startsWith(".")) {
247
+ return {
248
+ ok: false,
249
+ reason: `枚举 "${name}" 来自 npm 包——构建期常量折叠只支持 Stories.src 内的枚举,请把枚举放进源目录再相对导入`,
250
+ };
251
+ }
252
+ const fileRel = resolveEnumFileRel(imported.specifier, ctx);
253
+ if (fileRel === undefined) {
254
+ return {
255
+ ok: false,
256
+ reason: `无法解析枚举导入 "${imported.specifier}"(源目录内找不到对应文件)`,
257
+ };
258
+ }
259
+ const cachedFile = ctx.enumFiles.get(fileRel);
260
+ if (cachedFile !== undefined) {
261
+ const cached = cachedFile.get(imported.exportedName);
262
+ if (cached !== undefined) return { ok: true, value: cached };
263
+ return {
264
+ ok: false,
265
+ reason: `"${fileRel}" 中没有名为 "${imported.exportedName}" 的枚举`,
266
+ };
267
+ }
268
+ const parsed = parseEnumFile(fileRel, ctx);
269
+ ctx.enumFiles.set(fileRel, parsed);
270
+ const table = parsed.get(imported.exportedName);
271
+ if (table === undefined) {
272
+ return {
273
+ ok: false,
274
+ reason: `"${fileRel}" 中没有名为 "${imported.exportedName}" 的枚举`,
275
+ };
276
+ }
277
+ return { ok: true, value: table };
278
+ }
279
+
280
+ /** import 说明符 → 源根内 posix 路径(相对当前模块目录归一;补 .ts / index.ts) */
281
+ function resolveEnumFileRel(
282
+ specifier: string,
283
+ ctx: ConstExprContext,
284
+ ): string | undefined {
285
+ const base = posix.normalize(posix.join(posix.dirname(ctx.rel), specifier));
286
+ const candidates = base.endsWith(".ts")
287
+ ? [base]
288
+ : [`${base}.ts`, `${base}/index.ts`];
289
+ return candidates.find((cand) => existsSync(join(ctx.sourcesDir, cand)));
290
+ }
291
+
292
+ /** 读盘折叠一个枚举文件的全部 enum 声明(成员表按文法折叠,引用型初始化 fail-closed) */
293
+ function parseEnumFile(
294
+ fileRel: string,
295
+ ctx: ConstExprContext,
296
+ ): Map<string, EnumTable> {
297
+ const out = new Map<string, EnumTable>();
298
+ let text: string;
299
+ try {
300
+ text = readFileSync(join(ctx.sourcesDir, fileRel), "utf8");
301
+ } catch {
302
+ return out;
303
+ }
304
+ const sf = ts.createSourceFile(fileRel, text, ts.ScriptTarget.Latest, true);
305
+ for (const stmt of sf.statements) {
306
+ if (!ts.isEnumDeclaration(stmt)) continue;
307
+ const table = foldEnumMembers(stmt);
308
+ if (table !== undefined) out.set(stmt.name.text, table);
309
+ }
310
+ return out;
311
+ }
312
+
313
+ /** 折叠一个 enum 声明的成员表;不可折叠成员(引用/计算名等)= 整枚举不登记 */
314
+ function foldEnumMembers(stmt: ts.EnumDeclaration): EnumTable | undefined {
315
+ const table = new Map<string, string | number>();
316
+ let auto = 0;
317
+ let sawAuto = false;
318
+ for (const member of stmt.members) {
319
+ if (!ts.isIdentifier(member.name)) return undefined;
320
+ const init = member.initializer;
321
+ if (init === undefined) {
322
+ if (sawAuto) {
323
+ table.set(member.name.text, auto);
324
+ auto += 1;
325
+ } else if (table.size === 0) {
326
+ sawAuto = true;
327
+ table.set(member.name.text, 0);
328
+ auto = 1;
329
+ } else {
330
+ return undefined; // 字符串枚举的无初始化成员(TS 本身也报错)
331
+ }
332
+ continue;
333
+ }
334
+ const value = foldEnumInit(init);
335
+ if (value === undefined) return undefined;
336
+ if (typeof value === "number") {
337
+ sawAuto = true;
338
+ auto = value + 1;
339
+ }
340
+ table.set(member.name.text, value);
341
+ }
342
+ return table;
343
+ }
344
+
345
+ /** 枚举成员初始化器:字符串/数字字面量或带符号数字字面量;其余不支持 */
346
+ function foldEnumInit(init: ts.Expression): string | number | undefined {
347
+ const unwrapped = unwrapExpr(init);
348
+ if (ts.isStringLiteral(unwrapped)) return unwrapped.text;
349
+ if (ts.isNumericLiteral(unwrapped)) return Number(unwrapped.text);
350
+ if (
351
+ ts.isPrefixUnaryExpression(unwrapped) &&
352
+ unwrapped.operator === ts.SyntaxKind.MinusToken
353
+ ) {
354
+ const operand = unwrapExpr(unwrapped.operand);
355
+ if (ts.isNumericLiteral(operand)) return -Number(operand.text);
356
+ }
357
+ return undefined;
358
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * const-eval 域共享契约:JSON 可序列化值、求值两态结果、define 调用与非法调用记录。
3
+ * 独立成文件是为了让入口 `./const-eval.ts` 与 worker 轮次 `./round/` 引用同一份声明,
4
+ * 又不产生「入口 ↔ 轮次」相互 import(环判据白名单为空)。
5
+ */
6
+ import type { CompilerDiagnostic } from "../compiler/index.ts";
7
+
8
+ /** JSON 可序列化值(求值产物的唯一合法形态)。 */
9
+ export type JsonValue =
10
+ string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
11
+
12
+ /** 求值结果:要么拿到全部导出常量的可序列化值,要么拿到一组诊断(fail-closed)。 */
13
+ export type ConstEvalResult =
14
+ | { values: Record<string, JsonValue>; defineCalls: readonly DefineCall[] }
15
+ | { diags: CompilerDiagnostic[] };
16
+
17
+ /**
18
+ * define() 构建期内建的调用记录(形态 B 累积;书写序即合并序):
19
+ * - `path`:`define("a.b", value)` 点路径单值声明;
20
+ * - `spec`:`define({...})` 对象字面量子树声明。
21
+ * 值在 worker 内已过 classify(JSON 安全);非法值不进此表(走 defineBad 诊断)。
22
+ */
23
+ export type DefineCall =
24
+ | { kind: "path"; path: string; value: JsonValue }
25
+ | { kind: "spec"; spec: Record<string, JsonValue> };
26
+
27
+ /** define() 非法调用的记录(求值段把每条翻成诊断,统一挂入口 1:1)。 */
28
+ export interface DefineBad {
29
+ readonly path: string;
30
+ readonly reason: string;
31
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * 形态 B(define 累积)合并器:把求值期按书写序收集的 define 调用合并为一棵
3
+ * 纯嵌套树,供 state 降维段与 config.state(形态 A)走同一条 flattenRoot 通道。
4
+ *
5
+ * 合并文法(形态 B):
6
+ * - 点路径切段折叠;空段拒("a..b"、".a"、"a." 一律 fail-closed);
7
+ * - 同键二次定义 fail-closed(不后写覆盖);叶值与分支对象两向同键都算形状冲突;
8
+ * - 对象实参子树递归合并(同分支不同叶合法);
9
+ * - 对象字面量里的点键不在此拦——留给降维段的统一 dotKeyError 守卫;
10
+ * - 数组值是叶子(裸数组拒绝发生在降维段:请用 descriptor);
11
+ * - 冲突一次报全部(fail-closed,求值毕统一结算)。
12
+ *
13
+ * 多源汇入(mergeDefineCallsInto):config.ts 的 define 树作为底树,模块层
14
+ * define 带 `文件:行:列` 落点汇入;冲突文案区分单位置(仅本次来源)与两处位置
15
+ * (既有来源 与 本次来源),全部路径的来源表随插入增量登记。
16
+ */
17
+ import type { DefineCall } from "./const-eval.ts";
18
+
19
+ export interface DefineMergeResult {
20
+ /** 合并后的纯嵌套树(只含 define 贡献;无调用 = 空对象) */
21
+ readonly tree: Record<string, unknown>;
22
+ /** 冲突清单(逐条可读;非空 = fail-closed) */
23
+ readonly conflicts: readonly string[];
24
+ }
25
+
26
+ /** 带落点的 define 调用(模块层收集器产物;与 LocatedDefineCall 结构兼容) */
27
+ export interface OriginatedDefineCall {
28
+ readonly call: DefineCall;
29
+ readonly origin: string;
30
+ }
31
+
32
+ /** 跨文件来源登记:路径 → `文件:行:列`(config 段恒为 "config.ts") */
33
+ interface MergeContext {
34
+ readonly origins: Map<string, string>;
35
+ readonly origin: string;
36
+ }
37
+
38
+ const CONFIG_ORIGIN = "config.ts";
39
+
40
+ export function mergeDefineCalls(
41
+ calls: readonly DefineCall[],
42
+ ): DefineMergeResult {
43
+ const tree: Record<string, unknown> = {};
44
+ const conflicts: string[] = [];
45
+ for (const call of calls) {
46
+ if (call.kind === "path") {
47
+ insertSegments(
48
+ tree,
49
+ call.path.split("."),
50
+ call.value,
51
+ call.path,
52
+ conflicts,
53
+ );
54
+ } else {
55
+ mergeInto(tree, call.spec, [], conflicts);
56
+ }
57
+ }
58
+ return { tree, conflicts };
59
+ }
60
+
61
+ /**
62
+ * 把带落点的模块层 define 调用汇入既有合并树(就地变异)。
63
+ * 返回冲突清单(非空 = fail-closed,编排层一次报全部)。
64
+ */
65
+ export function mergeDefineCallsInto(
66
+ tree: Record<string, unknown>,
67
+ calls: readonly OriginatedDefineCall[],
68
+ ): readonly string[] {
69
+ const conflicts: string[] = [];
70
+ const origins = new Map<string, string>();
71
+ seedOrigins(tree, [], origins);
72
+ for (const item of calls) {
73
+ const ctx: MergeContext = { origins, origin: item.origin };
74
+ if (item.call.kind === "path") {
75
+ insertSegments(
76
+ tree,
77
+ item.call.path.split("."),
78
+ item.call.value,
79
+ item.call.path,
80
+ conflicts,
81
+ ctx,
82
+ );
83
+ } else {
84
+ mergeInto(tree, item.call.spec, [], conflicts, ctx);
85
+ }
86
+ seedOrigins(tree, [], origins);
87
+ }
88
+ return conflicts;
89
+ }
90
+
91
+ /** 全树路径登记来源(先见保留:config 底树先登记,新增路径归本次调用) */
92
+ function seedOrigins(
93
+ tree: Record<string, unknown>,
94
+ prefix: readonly string[],
95
+ origins: Map<string, string>,
96
+ ): void {
97
+ for (const [key, value] of Object.entries(tree)) {
98
+ const path = [...prefix, key].join(".");
99
+ if (!origins.has(path)) origins.set(path, CONFIG_ORIGIN);
100
+ if (isBranch(value)) seedOrigins(value, [...prefix, key], origins);
101
+ }
102
+ }
103
+
104
+ function unitSuffix(ctx: MergeContext | undefined): string {
105
+ return ctx === undefined ? "" : `(${ctx.origin})`;
106
+ }
107
+
108
+ function pairSuffix(path: string, ctx: MergeContext | undefined): string {
109
+ if (ctx === undefined) return "";
110
+ const existing = ctx.origins.get(path);
111
+ return existing === undefined
112
+ ? `(${ctx.origin})`
113
+ : `——${existing} 与 ${ctx.origin}`;
114
+ }
115
+
116
+ /** 把 `value` 按段序列插入树:中途遇叶子 = 叶↔分支冲突;终点同键 = 二次定义冲突。 */
117
+ function insertSegments(
118
+ tree: Record<string, unknown>,
119
+ segments: readonly string[],
120
+ value: unknown,
121
+ displayPath: string,
122
+ conflicts: string[],
123
+ ctx?: MergeContext,
124
+ ): void {
125
+ if (segments.some((segment) => segment === "")) {
126
+ conflicts.push(
127
+ `define("${displayPath}"):点路径含空段("a..b"、".a"、"a." 形态不支持)${unitSuffix(ctx)}`,
128
+ );
129
+ return;
130
+ }
131
+ let current: Record<string, unknown> = tree;
132
+ for (let i = 0; i < segments.length - 1; i++) {
133
+ const segment = segments[i]!;
134
+ const existing = current[segment];
135
+ if (existing === undefined) {
136
+ const next: Record<string, unknown> = {};
137
+ current[segment] = next;
138
+ current = next;
139
+ } else if (isBranch(existing)) {
140
+ current = existing;
141
+ } else {
142
+ const path = segments.slice(0, i + 1).join(".");
143
+ conflicts.push(
144
+ `defines["${path}"]:键已是叶子值,不能再作为分支下钻(define("${displayPath}"))${pairSuffix(path, ctx)}`,
145
+ );
146
+ return;
147
+ }
148
+ }
149
+ const leaf = segments[segments.length - 1]!;
150
+ const existing = current[leaf];
151
+ if (existing !== undefined) {
152
+ if (isBranch(existing) && isBranch(value)) {
153
+ mergeInto(existing, value, segments.slice(0, -1), conflicts, ctx);
154
+ return;
155
+ }
156
+ const path = segments.join(".");
157
+ conflicts.push(
158
+ `defines["${path}"]:同键二次定义(fail-closed,不后写覆盖)${pairSuffix(path, ctx)}`,
159
+ );
160
+ return;
161
+ }
162
+ current[leaf] = value;
163
+ }
164
+
165
+ /** 对象实参子树合并:同分支不同叶合法;同叶/形状冲突按叶路径报。 */
166
+ function mergeInto(
167
+ target: Record<string, unknown>,
168
+ source: Record<string, unknown>,
169
+ segments: readonly string[],
170
+ conflicts: string[],
171
+ ctx?: MergeContext,
172
+ ): void {
173
+ for (const key of Object.keys(source)) {
174
+ const incoming = source[key];
175
+ const existing = target[key];
176
+ const path = [...segments, key].join(".");
177
+ if (existing === undefined) {
178
+ target[key] = incoming;
179
+ } else if (isBranch(existing) && isBranch(incoming)) {
180
+ mergeInto(existing, incoming, [...segments, key], conflicts, ctx);
181
+ } else if (isBranch(existing) || isBranch(incoming)) {
182
+ conflicts.push(
183
+ `defines["${path}"]:形状冲突——叶值与分支对象不能同键(fail-closed)${pairSuffix(path, ctx)}`,
184
+ );
185
+ } else {
186
+ conflicts.push(
187
+ `defines["${path}"]:同键二次定义(fail-closed,不后写覆盖)${pairSuffix(path, ctx)}`,
188
+ );
189
+ }
190
+ }
191
+ }
192
+
193
+ function isBranch(value: unknown): value is Record<string, unknown> {
194
+ return typeof value === "object" && value !== null && !Array.isArray(value);
195
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 两轮求值的逐键比较:确定性闸门的判据。
3
+ */
4
+ import type { JsonValue } from "../contracts.ts";
5
+
6
+ export function firstDeterminismDiff(
7
+ a: Record<string, JsonValue>,
8
+ b: Record<string, JsonValue>,
9
+ ): string | null {
10
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
11
+ for (const key of keys) {
12
+ if (!deepEqual(a[key], b[key])) return key;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export function deepEqual(a: unknown, b: unknown): boolean {
18
+ if (a === b) return true;
19
+ if (a === null || b === null || typeof a !== typeof b) return false;
20
+ if (Array.isArray(a) || Array.isArray(b)) {
21
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
22
+ return false;
23
+ return a.every((item, index) => deepEqual(item, b[index]));
24
+ }
25
+ if (typeof a === "object" && typeof b === "object") {
26
+ const keysA = Object.keys(a);
27
+ const keysB = Object.keys(b);
28
+ if (keysA.length !== keysB.length) return false;
29
+ return keysA.every(
30
+ (key) =>
31
+ key in b &&
32
+ deepEqual(
33
+ (a as { [k: string]: JsonValue })[key],
34
+ (b as { [k: string]: JsonValue })[key],
35
+ ),
36
+ );
37
+ }
38
+ return false;
39
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 诊断辅助出口(具名转发,不用 `export *`)。
3
+ */
4
+ export { locateExport } from "./locate.ts";
5
+ export { firstDeterminismDiff, deepEqual } from "./compare.ts";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 诊断定位:在入口源里定位 `export <key>` 的 1 基 line/col。
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+
6
+ /** 在入口源文件里定位 `export <key>` 声明(1 基 line/col);找不到退回 1:1。 */
7
+ export function locateExport(
8
+ absEntry: string,
9
+ key: string,
10
+ ): { line: number; col: number } {
11
+ let text: string;
12
+ try {
13
+ text = readFileSync(absEntry, "utf8");
14
+ } catch {
15
+ return { line: 1, col: 1 };
16
+ }
17
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ const patterns =
19
+ key === "default"
20
+ ? [/(^|\n)[ \t]*export[ \t]+default\b/]
21
+ : [
22
+ new RegExp(
23
+ `(^|\\n)[ \\t]*export[ \t]+(?:const|let|var|async[ \t]+function|function|class)[ \t]+${escaped}\\b`,
24
+ ),
25
+ new RegExp(
26
+ `(^|\\n)[ \\t]*export[ \t]*\\{[^}]*\\b${escaped}\\b[^}]*\\}`,
27
+ ),
28
+ ];
29
+ for (const pattern of patterns) {
30
+ const match = pattern.exec(text);
31
+ if (match !== null) return offsetToPos(text, match.index + match[1].length);
32
+ }
33
+ return { line: 1, col: 1 };
34
+ }
35
+
36
+ export function offsetToPos(
37
+ text: string,
38
+ offset: number,
39
+ ): { line: number; col: number } {
40
+ const lines = text.slice(0, offset).split("\n");
41
+ return {
42
+ line: lines.length,
43
+ col: (lines[lines.length - 1] ?? "").length + 1,
44
+ };
45
+ }