@lingfan-engine/stories-build 0.0.0-stage → 0.1.2

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,18 @@
1
+ /**
2
+ * 流水线域:把各域串成一次构建,并做写盘前的强制校验。
3
+ *
4
+ * `orchestrate` 是整条链的主入口(也是包出口导出 `buildStories` 的来源),
5
+ * `gates` 是它在写盘前调用的最后一道校验——两者同处一层,依赖方向单向朝下。
6
+ */
7
+ export {
8
+ buildStories,
9
+ DEFAULT_GENERATED_DIR,
10
+ DEFAULT_RESOURCES_DIR,
11
+ DEFAULT_SOURCES_DIR,
12
+ type StoryBuildOptions,
13
+ } from "./orchestrate.ts";
14
+ export {
15
+ assertKnownGuards,
16
+ assertKnownOps,
17
+ type GateOptions,
18
+ } from "./gates.ts";
@@ -0,0 +1,459 @@
1
+ import { join } from "node:path";
2
+ import {
3
+ STORIES_DIR,
4
+ type Story,
5
+ type StoryColumn,
6
+ } from "@lingfan-engine/core";
7
+ import { TS_SOURCE_MANIFEST_KEY } from "@lingfan-engine/editor";
8
+ import {
9
+ defaultStoriesBuildTexts,
10
+ StoryBuildError,
11
+ type StoriesBuildTexts,
12
+ } from "../shared/index.ts";
13
+ import { assertKnownGuards, assertKnownOps } from "./gates.ts";
14
+ import { scanCellSources } from "../compiler/index.ts";
15
+ import { runConstEvalPass } from "../const-eval/index.ts";
16
+ import { assertRoundTrip, compileStory } from "../emit/index.ts";
17
+ import {
18
+ loadDefaultCompiler,
19
+ type CompiledStoryEntry,
20
+ type StoriesCompiler,
21
+ } from "../compiler/index.ts";
22
+ import { emitArtifacts } from "../emit/index.ts";
23
+ import { loadExtensions } from "../compiler/index.ts";
24
+ import { readManifest, CONFIG_FILE } from "../config/index.ts";
25
+ import { drainBuildWarnings, type BuildReport } from "../emit/index.ts";
26
+ import {
27
+ collectModuleCharacters,
28
+ collectModuleDefines,
29
+ evalConfigModule,
30
+ mergeDefineCallsInto,
31
+ runStateSchemaPass,
32
+ type LocatedDefineCall,
33
+ } from "../const-eval/index.ts";
34
+ import { discoverSources } from "../discovery/index.ts";
35
+ import {
36
+ FONTS_DIR_NAME,
37
+ fontNameSet,
38
+ scanFontRegistry,
39
+ } from "../fonts/index.ts";
40
+ import {
41
+ collectDeclaredNames,
42
+ collectEnumTypeSlotsForBuild,
43
+ renderFontTypegen,
44
+ renderScriptTypegen,
45
+ renderStateTypegen,
46
+ renderTypegen,
47
+ } from "../typegen/index.ts";
48
+ import { walkFiles } from "../shared/index.ts";
49
+ import { DEFAULT_EVAL_TIMEOUT_MS } from "../const-eval/index.ts";
50
+
51
+ /** 构建产物的目录名与生成物文件名(默认值 = 参考宿主的现状目录名) */
52
+ export const DEFAULT_SOURCES_DIR = "Stories.src";
53
+ /** 资源目录名(相对工程根):清单与 `Stories/**` 产物落在这下面。 */
54
+ export const DEFAULT_RESOURCES_DIR = "Resources";
55
+ /** 生成物目录名(相对源目录):`fun_register.g.ts` 落在这里。 */
56
+ export const DEFAULT_GENERATED_DIR = "gen";
57
+
58
+ /**
59
+ * 构建的可配置项:只列目录名这类工程布局事实(把同一工程挪到别的目录名只改这里)。
60
+ * 整项缺省 = 现状布局;这些名字同时进失败文案,故改值即改文案,默认下逐字不变。
61
+ */
62
+ export interface StoryBuildOptions {
63
+ /** 源目录名(相对工程根) */
64
+ readonly sourcesDir?: string;
65
+ /** 资源目录名(相对工程根;清单与 `Stories/**` 都住在它下面) */
66
+ readonly resourcesDir?: string;
67
+ /** 生成物目录名(相对源目录;`fun_register.g.ts` 落在这里) */
68
+ readonly generatedDir?: string;
69
+ /** 编译器注入(过渡期/测试用);缺省 = 装载 `compiler` 域的默认实现 */
70
+ readonly compiler?: StoriesCompiler;
71
+ /** 文案注入(Partial);缺省 = 内建缺省文案(即构建层的既有文案) */
72
+ readonly texts?: Partial<StoriesBuildTexts>;
73
+ /** 编译期求值单轮时限(毫秒;缺省 30_000,只影响求值超时判定与超时文案) */
74
+ readonly evalTimeoutMs?: number;
75
+ }
76
+
77
+ /** 生成物文件名(唯一一份;报告里的逻辑路径由它和源/生成物目录名拼出) */
78
+ const GENERATED_FILE = "fun_register.g.ts";
79
+ /** 类型面生成物文件名(列名与插值键的字面量收窄面,供 IDE 与 typecheck 用) */
80
+ const TYPEGEN_FILE = "stories.gen.d.ts";
81
+ /** state 类型面生成物文件名(StoryState 精确类型,形态 B 的 ambient 桥引它) */
82
+ const STATE_TYPEGEN_FILE = "state.gen.d.ts";
83
+ /** script 命名空间 ambient 声明文件名(免导入直呼的类型面,内容恒定) */
84
+ const SCRIPT_TYPEGEN_FILE = "script.gen.d.ts";
85
+ /** font 字面量类型面文件名(字体名注册表的键=值收窄面,内容随注册表) */
86
+ const FONT_TYPEGEN_FILE = "font.gen.d.ts";
87
+
88
+ /**
89
+ * 编译 `root/Stories.src/` 下的 TS 源 → `root/Resources/`(纯编排,fs 全在参数 root 之下;
90
+ * 测试用临时工程根直测,CLI 主守卫传真实根)。
91
+ *
92
+ * 十四段串行:config.ts 单次求值(named exports 四导出 folders/start/vars/state +
93
+ * 形态 B define 累积,读取/校验统一在此)→ 源发现(folders 多目录 + start 全局恰一)
94
+ * → cell 扫描 → 编译期求值(node_modules 值导入的模块整体求值,导出常量喂给
95
+ * 编译器内联)→ 模块层构建期 define 收集汇入(顶层 define 语句与 config 树同源合并,
96
+ * 互斥/冲突 fail-closed)与 state 降维(config.state/define 合并树走同一降维文法
97
+ * 扁平化为 defines)→ 字体名注册表扫描(Resources/Fonts 文件名去扩展名,编译期字体
98
+ * 引用检查对照源)→ story-compiler 编译(诊断非空 fail-closed)→ parseStory 校验兜底 →
99
+ * 列归一与 sourcePath 回填(源文件路径 = 产物路径)→ 工程合并(id/entry 取 start 故事,
100
+ * defines 冲突 fail-closed)→ 清单读取 → 扩展装载 → op/守卫闸门 → 编译与往返自检
101
+ * → 差量写盘 → 生成物与警告收集。交错的顺序本身即契约(前置段的失败文案要先于
102
+ * 后置段发出),调整即改变可观测行为。
103
+ */
104
+ export async function buildStories(
105
+ root: string,
106
+ options: StoryBuildOptions = {},
107
+ ): Promise<BuildReport> {
108
+ const sourcesDirName = options.sourcesDir ?? DEFAULT_SOURCES_DIR;
109
+ const resourcesDirName = options.resourcesDir ?? DEFAULT_RESOURCES_DIR;
110
+ const generatedDirName = options.generatedDir ?? DEFAULT_GENERATED_DIR;
111
+ // 文案与求值时限:入口处一次性合并/取缺省,下游各段只收完整对象/数值(纯透传)
112
+ const texts: StoriesBuildTexts = {
113
+ ...defaultStoriesBuildTexts,
114
+ ...options.texts,
115
+ };
116
+ const evalTimeoutMs = options.evalTimeoutMs ?? DEFAULT_EVAL_TIMEOUT_MS;
117
+
118
+ const sourcesDir = join(root, sourcesDirName);
119
+ const resourcesDir = join(root, resourcesDirName);
120
+ const storiesDir = join(resourcesDir, STORIES_DIR);
121
+
122
+ // —— config.ts 单次求值:named exports 四导出(folders/start/vars/state)+ 形态 B
123
+ // define 累积一并取出;布局校验(parseFolders/parseStart)、define 合并冲突与
124
+ // 形态 A/B 互斥全部 fail-closed(读取统一走本通道,图谱层不再解析 config)——
125
+ const evaluated = await evalConfigModule(
126
+ root,
127
+ sourcesDirName,
128
+ texts,
129
+ evalTimeoutMs,
130
+ );
131
+
132
+ // —— 源发现:config 布局声明 → folders 多目录递归 → start 全局恰一 ——
133
+ const discovery = await discoverSources(
134
+ root,
135
+ sourcesDirName,
136
+ evaluated.config,
137
+ texts,
138
+ );
139
+
140
+ // —— cell 声明扫描(构建期 AST 扫描):源树(排除生成物目录,仅 .ts;机制不动)。
141
+ // 源树同时是模块层 define 收集与枚举槽位回收的输入(只读复用,不重扫)——
142
+ const sourceTree = walkFiles(sourcesDir);
143
+ const cellScan = scanCellSources(sourceTree, generatedDirName);
144
+
145
+ // —— 编译期求值:含 node_modules 值导入的文件整体求值,导出常量值喂给
146
+ // 编译器内联;求值诊断与双求值确定性闸门 fail-closed,一次报全部 ——
147
+ const constValues = await runConstEvalPass(
148
+ sourcesDir,
149
+ discovery.stories,
150
+ discovery.modules,
151
+ sourcesDirName,
152
+ texts,
153
+ evalTimeoutMs,
154
+ );
155
+
156
+ // —— 模块层构建期 define 收集与汇入:源模块顶层 define 语句(函数体外)AST 折叠,
157
+ // 与 config 的 define 树汇入同一棵合并树(形态 B 的 define 域放宽)。形态 A
158
+ // (state 导出)与模块层 define 互斥;跨文件同键/形状冲突 fail-closed 一次报
159
+ // 全部(落点带 源文件:行:列)——
160
+ const moduleDefineCalls = collectModuleDefines({
161
+ sourcesDir,
162
+ files: sourceTree,
163
+ generatedDirName,
164
+ });
165
+ if (evaluated.state !== undefined && moduleDefineCalls.length > 0) {
166
+ throw new StoryBuildError(
167
+ [
168
+ `${sourcesDirName}/${CONFIG_FILE}: 形态 A(state 导出)与模块层构建期 define 互斥——形态 A/B 二选一:`,
169
+ ...moduleDefineCalls.map(
170
+ (call) => `- ${call.origin}${defineLabel(call)}`,
171
+ ),
172
+ ].join("\n"),
173
+ );
174
+ }
175
+ const moduleDefineConflicts = mergeDefineCallsInto(
176
+ evaluated.defineTree,
177
+ moduleDefineCalls,
178
+ );
179
+ if (moduleDefineConflicts.length > 0) {
180
+ throw new StoryBuildError(
181
+ `模块层构建期 define 冲突(${moduleDefineConflicts.length} 条):\n${moduleDefineConflicts
182
+ .map((conflict) => `- ${conflict}`)
183
+ .join("\n")}`,
184
+ );
185
+ }
186
+
187
+ // —— 模块层构建期角色声明收集:源模块顶层 character 语句折叠成全工程角色表
188
+ // (同名不同义 fail-closed,全等幂等),喂给编译器前置进每个故事 start 列头 ——
189
+ const moduleCharacters = collectModuleCharacters({
190
+ sourcesDir,
191
+ files: sourceTree,
192
+ generatedDirName,
193
+ });
194
+
195
+ // —— state 降维:config.state(形态 A)与 define 合并树(形态 B,二选一)
196
+ // 走同一降维文法扁平化为 defines;文法诊断 fail-closed,皆缺省 = 空 schema 贡献 ——
197
+ const schemaDefines = runStateSchemaPass(
198
+ evaluated.state,
199
+ evaluated.defineTree,
200
+ sourcesDirName,
201
+ );
202
+
203
+ // —— 字体名注册表:Resources/Fonts 下的字体文件名去扩展名(目录缺失/为空 = 空注册表,
204
+ // 引用字体名的工程 fail-closed,未引用的零打扰);喂给编译器做 {font=} 标签与角色
205
+ // 字体的构建期检查,宿主侧由 Vite 扫描同目录注入 @font-face ——
206
+ const fontEntries = scanFontRegistry(join(resourcesDir, FONTS_DIR_NAME));
207
+ const fontNames = fontNameSet(fontEntries);
208
+
209
+ // —— story-compiler:故事/模块清单一次性编译(诊断非空 = fail-closed,一次报全部)——
210
+ const compiler = options.compiler ?? (await loadDefaultCompiler());
211
+ const compiled = await compiler.compileStories({
212
+ root,
213
+ sourcesDir,
214
+ config: evaluated.config,
215
+ stories: discovery.stories,
216
+ modules: discovery.modules,
217
+ constValues,
218
+ fontNames,
219
+ characters: moduleCharacters,
220
+ texts,
221
+ });
222
+ if (compiled.diags.length > 0) {
223
+ throw new StoryBuildError(
224
+ `故事编译失败(${compiled.diags.length} 条):\n${compiled.diags
225
+ .map(
226
+ (diag) => `- ${diag.file}:${diag.line}:${diag.col} ${diag.message}`,
227
+ )
228
+ .join("\n")}`,
229
+ );
230
+ }
231
+
232
+ // —— 列归一:产物路径 = 源文件路径(`Stories/<相对源路径去扩展名>.json`;
233
+ // 文件名与列 id 解耦)。parseStory 不在链上:编译器静态产出 Story
234
+ // 同形,形状契约由防御读取 + serialize/assemble 既有校验兜底。——
235
+ const bySource = new Map<string, { story: Story; columns: StoryColumn[] }>();
236
+ for (const entry of compiled.stories) {
237
+ const { story, column } = readCompiledEntry(entry, sourcesDirName);
238
+ const group = bySource.get(entry.sourceRelPath);
239
+ if (group === undefined) {
240
+ bySource.set(entry.sourceRelPath, { story, columns: [column] });
241
+ } else {
242
+ group.columns.push(column);
243
+ }
244
+ }
245
+
246
+ // —— 工程级合并:id/entry 取 start 故事;defines 跨文件冲突 fail-closed ——
247
+ const startGroup = bySource.get(discovery.startSource);
248
+ if (startGroup === undefined) {
249
+ throw new StoryBuildError(
250
+ `${sourcesDirName}/${discovery.startSource}: 入口故事未产出任何列(compileStories 契约破坏)`,
251
+ );
252
+ }
253
+ const merged = startGroup.story;
254
+ merged.columns = [...bySource.values()].flatMap((group) => group.columns);
255
+ merged.defines = mergeDefines(bySource, sourcesDirName, schemaDefines);
256
+ // 故事版本(作者声明,缺省不写):进存档 storyVersion;托管键更新把声明写进 project.json
257
+ merged.version = evaluated.config.version;
258
+
259
+ // —— 工程清单:先有 project.json(defines/shell 是工程事实,不归 TS 源管)——
260
+ // 标记键 `tsSource`(非托管键,serializeProject 原位保留):编辑器据此对
261
+ // `Stories/**` 与 `project.json` 只读(守卫判据,构建写入、编辑器不碰)。
262
+ const manifest = readManifest(root, resourcesDirName);
263
+ const markedManifest = {
264
+ ...(typeof manifest === "object" && manifest !== null ? manifest : {}),
265
+ [TS_SOURCE_MANIFEST_KEY]: true,
266
+ };
267
+
268
+ // —— 扩展声明装载(对齐运行期声明制:同一份 manifest.extensions、同一个装载契约)——
269
+ const { declared, opNames, ids } = await loadExtensions(root, manifest);
270
+
271
+ const gateOptions = {
272
+ resourcesDir: resourcesDirName,
273
+ sourcesDir: sourcesDirName,
274
+ texts,
275
+ };
276
+ // —— op 闸门与守卫名闸门(两条 fail-early 判定,各自带定位一次报全部)——
277
+ assertKnownOps(merged, opNames, gateOptions);
278
+ assertKnownGuards(
279
+ merged,
280
+ new Set(cellScan.guards.map((guard) => guard.name)),
281
+ gateOptions,
282
+ );
283
+
284
+ // —— 编译与往返自检(config.start → project.json entry 由托管键更新落地)——
285
+ const serialized = compileStory(merged, markedManifest);
286
+ assertRoundTrip(serialized);
287
+
288
+ // —— 类型面生成物:字符串域全部取上面已算好的结果,不重跑任何一段 ——
289
+ // 列 id 取编译产物、state 键取 schemaDefines、变量键取 config.vars 导出的键集
290
+ // (config.ts 求值通道;图谱层的 defineVars 扫描已退役为守卫),声明名从列的
291
+ // 命令流里收(let/for/input 这类运行期变量名只存在于产物里),
292
+ // 扩展 id 取扩展装载段的装载结果(装载在写盘之前,数据现成)。
293
+ const typegenContent = renderTypegen({
294
+ columns: merged.columns.map((column) => column.id),
295
+ schemaKeys: Object.keys(schemaDefines),
296
+ defineKeys: [...evaluated.vars],
297
+ declaredNames: collectDeclaredNames(merged.columns),
298
+ extensionPrefixes: ids,
299
+ });
300
+
301
+ // —— state 类型面生成物(形态 B gen 类型面):树取求值段结果(形态 A state 树 /
302
+ // 形态 B define 合并树——模块层 define 已在前置段汇入同一棵树);枚举 type 槽位
303
+ // 从 config.ts 与各模块源文按点路径回收(esbuild 求值丢标识符,AST 侧补回收)。
304
+ // 渲染恒产出:空树退化为 Record<string, never>,ambient 桥永远有类型可引 ——
305
+ const stateTypegenContent = renderStateTypegen({
306
+ tree: evaluated.state ?? evaluated.defineTree,
307
+ enumSlots: collectEnumTypeSlotsForBuild({
308
+ configSource: sourceTree.get(CONFIG_FILE) ?? "",
309
+ moduleFiles: moduleSlotFiles(sourceTree, moduleDefineCalls),
310
+ }),
311
+ generatedDir: generatedDirName,
312
+ });
313
+
314
+ // —— script 命名空间 ambient 声明(免导入直呼):内容恒定、与工程状态无关,
315
+ // 纯编译期类型面零运行时注入;显式 import / 解构按 TS 遮蔽语义优先 ——
316
+ const scriptTypegenContent = renderScriptTypegen();
317
+
318
+ // —— font 字面量类型面:键 = 值 = 注册表字体名(引号键保非标识符名);
319
+ // 空注册表恒产出封闭空对象,未注册名在 TS 面不可见(改拼即 TS2551)——
320
+ const fontTypegenContent = renderFontTypegen(fontEntries);
321
+
322
+ // —— 差量写盘 + 生成物(逻辑路径,相对资源根)——
323
+ const { added, updated, removed } = emitArtifacts({
324
+ root,
325
+ resourcesDir,
326
+ storiesDir,
327
+ generatedRel: `${sourcesDirName}/${generatedDirName}/${GENERATED_FILE}`,
328
+ serialized,
329
+ cellScan,
330
+ typegen: {
331
+ rel: `${sourcesDirName}/${generatedDirName}/${TYPEGEN_FILE}`,
332
+ content: typegenContent,
333
+ },
334
+ stateGen: {
335
+ rel: `${sourcesDirName}/${generatedDirName}/${STATE_TYPEGEN_FILE}`,
336
+ content: stateTypegenContent,
337
+ },
338
+ scriptGen: {
339
+ rel: `${sourcesDirName}/${generatedDirName}/${SCRIPT_TYPEGEN_FILE}`,
340
+ content: scriptTypegenContent,
341
+ },
342
+ fontGen: {
343
+ rel: `${sourcesDirName}/${generatedDirName}/${FONT_TYPEGEN_FILE}`,
344
+ content: fontTypegenContent,
345
+ },
346
+ });
347
+
348
+ return {
349
+ source: discovery.startSource,
350
+ storyId: merged.id,
351
+ columns: merged.columns.length,
352
+ added,
353
+ updated,
354
+ removed,
355
+ extensions: declared,
356
+ functions: cellScan.guards.map((guard) => guard.name),
357
+ warnings: drainBuildWarnings(),
358
+ };
359
+ }
360
+
361
+ function isRecord(value: unknown): value is Record<string, unknown> {
362
+ return typeof value === "object" && value !== null && !Array.isArray(value);
363
+ }
364
+
365
+ /** 互斥文案里的一条调用签名(path 形带键名;spec 形统一签名) */
366
+ function defineLabel(call: LocatedDefineCall): string {
367
+ return call.call.kind === "path"
368
+ ? `define("${call.call.path}")`
369
+ : "define({…})";
370
+ }
371
+
372
+ /** 枚举槽位回收的模块源文(按调用落点去重排序;树里缺文本的文件跳过) */
373
+ function moduleSlotFiles(
374
+ sourceTree: ReadonlyMap<string, string>,
375
+ calls: readonly { rel: string }[],
376
+ ): Map<string, string> {
377
+ const files = new Map<string, string>();
378
+ for (const rel of [...new Set(calls.map((call) => call.rel))].sort()) {
379
+ const text = sourceTree.get(rel);
380
+ if (text !== undefined) files.set(rel, text);
381
+ }
382
+ return files;
383
+ }
384
+
385
+ /**
386
+ * 编译条目防御读取:取条目列(columnId 必须在产物列集中)、回填产物 sourcePath、
387
+ * 复核 story 骨架(columns 列集 + id/entry 字符串)。更深的合法性(列 id 安全段、
388
+ * 重复、entry 存在性等)交给 serializeProject/assembleProject 既有校验兜底。
389
+ */
390
+ function readCompiledEntry(
391
+ entry: CompiledStoryEntry,
392
+ sourcesDirName: string,
393
+ ): { story: Story; column: StoryColumn } {
394
+ const sourceLabel = `${sourcesDirName}/${entry.sourceRelPath}`;
395
+ if (!isRecord(entry.story) || !Array.isArray(entry.story.columns)) {
396
+ throw new StoryBuildError(
397
+ `${sourceLabel}: 编译器条目 story 缺少列集(compileStories 契约破坏)`,
398
+ );
399
+ }
400
+ const column = entry.story.columns.find(
401
+ (item) => isRecord(item) && item.id === entry.columnId,
402
+ );
403
+ if (column === undefined) {
404
+ throw new StoryBuildError(
405
+ `${sourceLabel}: 编译器条目 columnId "${entry.columnId}" 不在产物列集中(compileStories 契约破坏)`,
406
+ );
407
+ }
408
+ if (
409
+ typeof entry.story.id !== "string" ||
410
+ entry.story.id === "" ||
411
+ typeof entry.story.entry !== "string" ||
412
+ entry.story.entry === ""
413
+ ) {
414
+ throw new StoryBuildError(
415
+ `${sourceLabel}: 编译器条目 story 缺少 id/entry(compileStories 契约破坏)`,
416
+ );
417
+ }
418
+ const story = entry.story as unknown as Story;
419
+ const typedColumn = column as unknown as StoryColumn;
420
+ typedColumn.sourcePath = `${STORIES_DIR}/${entry.sourceRelPath.replace(/\.ts$/, ".json")}`;
421
+ return { story, column: typedColumn };
422
+ }
423
+
424
+ /**
425
+ * 跨来源的 defines 合并:同键同值收敛,同键异值 = 冲突 fail-closed(一次报全部)。
426
+ * 来源 = config 状态(config 先行:形态 A state 或形态 B define 合并树)+ 各故事文件;
427
+ * config 与故事同键异值同样冲突。
428
+ */
429
+ function mergeDefines(
430
+ bySource: ReadonlyMap<string, { story: Story; columns: StoryColumn[] }>,
431
+ sourcesDirName: string,
432
+ schemaDefines: Record<string, unknown> = {},
433
+ ): Record<string, unknown> {
434
+ const merged: Record<string, unknown> = {};
435
+ const conflicts: string[] = [];
436
+ const put = (key: string, value: unknown, source: string): void => {
437
+ if (key in merged && merged[key] !== value) {
438
+ conflicts.push(
439
+ `- defines["${key}"] 在 ${sourcesDirName}/${source} 与先前的声明不一致(${JSON.stringify(merged[key])} ≠ ${JSON.stringify(value)})`,
440
+ );
441
+ return;
442
+ }
443
+ merged[key] = value;
444
+ };
445
+ for (const [key, value] of Object.entries(schemaDefines)) {
446
+ put(key, value, "config.ts(state/define)");
447
+ }
448
+ for (const [source, { story }] of bySource) {
449
+ for (const [key, value] of Object.entries(story.defines ?? {})) {
450
+ put(key, value, source);
451
+ }
452
+ }
453
+ if (conflicts.length > 0) {
454
+ throw new StoryBuildError(
455
+ `跨故事文件的 defines 冲突(工程级 defines 只有一份,请统一到工程清单或消除同名异值):\n${conflicts.join("\n")}`,
456
+ );
457
+ }
458
+ return merged;
459
+ }