@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,202 @@
1
+ /**
2
+ * 函数编译:把 .ts 模块里的函数声明(列内嵌套/跨列被引)翻译成 func 命令,
3
+ * 含参数校验、嵌套函数提升与互调、结果缓存(按 `文件#拥有者链#名` 键)。
4
+ * 会话与列编译入口在 ./session.ts。
5
+ */
6
+ import * as ts from "typescript";
7
+ import { compareOrdinal, type StoryCommand } from "@lingfan-engine/core";
8
+ import type { DiagBag, Graph } from "../graph.ts";
9
+ import {
10
+ createBodyCtx,
11
+ makeFuncOp,
12
+ translateStatements,
13
+ type FuncRegistry,
14
+ type Scope,
15
+ type ScopeEntry,
16
+ } from "../translate.ts";
17
+ import type { AssembleSession, CompiledFunction } from "./types.ts";
18
+
19
+ function nodePos(
20
+ sf: ts.SourceFile,
21
+ node: ts.Node,
22
+ ): { file: string; line: number; col: number } {
23
+ const { line, character } = sf.getLineAndCharacterOfPosition(
24
+ node.getStart(sf),
25
+ );
26
+ return { file: sf.fileName, line: line + 1, col: character + 1 };
27
+ }
28
+
29
+ export function partitionFunctions(statements: readonly ts.Statement[]): {
30
+ fns: ts.FunctionDeclaration[];
31
+ rest: ts.Statement[];
32
+ } {
33
+ const fns: ts.FunctionDeclaration[] = [];
34
+ const rest: ts.Statement[] = [];
35
+ for (const stmt of statements) {
36
+ if (
37
+ ts.isFunctionDeclaration(stmt) &&
38
+ stmt.name !== undefined &&
39
+ stmt.body !== undefined
40
+ ) {
41
+ fns.push(stmt);
42
+ continue;
43
+ }
44
+ rest.push(stmt);
45
+ }
46
+ return { fns, rest };
47
+ }
48
+
49
+ function readParams(
50
+ ctx: ReturnType<typeof createBodyCtx>,
51
+ decl: ts.FunctionDeclaration,
52
+ ): string[] | undefined {
53
+ const params: string[] = [];
54
+ for (const param of decl.parameters) {
55
+ if (!ts.isIdentifier(param.name)) {
56
+ diagNode(ctx, param, "函数参数必须是简单标识符");
57
+ return undefined;
58
+ }
59
+ if (params.includes(param.name.text)) {
60
+ diagNode(ctx, param, `重复的参数名 \`${param.name.text}\``);
61
+ return undefined;
62
+ }
63
+ if (
64
+ param.initializer !== undefined ||
65
+ param.questionToken !== undefined ||
66
+ param.dotDotDotToken !== undefined
67
+ ) {
68
+ diagNode(ctx, param, "函数参数不支持默认值/可选/剩余");
69
+ return undefined;
70
+ }
71
+ params.push(param.name.text);
72
+ }
73
+ return params;
74
+ }
75
+
76
+ function diagNode(
77
+ ctx: ReturnType<typeof createBodyCtx>,
78
+ node: ts.Node,
79
+ message: string,
80
+ ): void {
81
+ ctx.shared.diags.add(ctx.shared.file, ctx.shared.mod.sf, node, message);
82
+ }
83
+
84
+ export function tryFuncOp(
85
+ sf: ts.SourceFile,
86
+ node: ts.Node,
87
+ diags: DiagBag,
88
+ name: string,
89
+ params: string[],
90
+ body: StoryCommand[],
91
+ ): StoryCommand | undefined {
92
+ try {
93
+ return makeFuncOp(name, params, body);
94
+ } catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ diags.add(
97
+ sf.fileName,
98
+ sf,
99
+ node,
100
+ `func 命令构建失败(${name}):${message}`,
101
+ );
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ export function compileFunction(
107
+ session: AssembleSession,
108
+ graph: Graph,
109
+ diags: DiagBag,
110
+ file: string,
111
+ ownerChain: readonly string[],
112
+ decl: ts.FunctionDeclaration,
113
+ parentScopes: readonly Scope[],
114
+ ): CompiledFunction | undefined {
115
+ const mod = graph.records.get(file);
116
+ if (mod === undefined || decl.name === undefined) return undefined;
117
+ const sf = mod.sf;
118
+ const name = decl.name.text;
119
+ const key = `${file}#${[...ownerChain, name].join("#")}`;
120
+ const cached = session.cache.get(key);
121
+ if (cached !== undefined) return cached;
122
+
123
+ const registry: FuncRegistry = { refs: new Map(), names: new Map() };
124
+ const shared = {
125
+ mod,
126
+ file,
127
+ diags,
128
+ funcs: registry,
129
+ stateRoot: graph.stateRoot,
130
+ fontNames: session.fontNames,
131
+ texts: session.texts,
132
+ };
133
+ const ctx = createBodyCtx(shared, { inFunction: true, parentScopes });
134
+ const params = readParams(ctx, decl);
135
+ if (params === undefined) return undefined;
136
+ const paramScope = new Map<string, ScopeEntry>(
137
+ params.map((p) => [p, { kind: "param" }]),
138
+ );
139
+ ctx.scopes.push(paramScope);
140
+
141
+ const { fns: nestedDecls, rest } = partitionFunctions(decl.body!.statements);
142
+ // 函数声明提升:嵌套名先登记进本层,体翻译即可引用它们,嵌套函数之间也能互调
143
+ for (const nestedDecl of nestedDecls) {
144
+ ctx.scopes[ctx.scopes.length - 1].set(nestedDecl.name!.text, {
145
+ kind: "function",
146
+ file,
147
+ });
148
+ }
149
+ const bodyCommands = translateStatements(ctx, rest);
150
+ // 嵌套函数体继承本层作用域链(参数/局部/嵌套名只读下传);本层翻译先完成,声明已齐备
151
+ const nested: CompiledFunction[] = [];
152
+ for (const nestedDecl of nestedDecls) {
153
+ const compiled = compileFunction(
154
+ session,
155
+ graph,
156
+ diags,
157
+ file,
158
+ [...ownerChain, name],
159
+ nestedDecl,
160
+ ctx.scopes,
161
+ );
162
+ if (compiled === undefined) continue;
163
+ nested.push(compiled);
164
+ }
165
+
166
+ const allNames = new Map<string, { file: string; line: number; col: number }>(
167
+ [[name, nodePos(sf, decl.name)]],
168
+ );
169
+ for (const item of nested) {
170
+ for (const [n, pos] of item.allNames) allNames.set(n, pos);
171
+ }
172
+ const refs = new Set<string>([
173
+ ...registry.refs.keys(),
174
+ ...nested.flatMap((item) => [...item.refs]),
175
+ ]);
176
+ const nestedOps: StoryCommand[] = [];
177
+ for (const item of [...nested].sort((a, b) =>
178
+ compareOrdinal(a.name, b.name),
179
+ )) {
180
+ const op = tryFuncOp(
181
+ sf,
182
+ decl.name,
183
+ diags,
184
+ item.name,
185
+ item.params,
186
+ item.body,
187
+ );
188
+ if (op === undefined) continue;
189
+ nestedOps.push(op);
190
+ }
191
+ const compiled: CompiledFunction = {
192
+ key,
193
+ file,
194
+ name,
195
+ params,
196
+ body: [...nestedOps, ...bodyCommands],
197
+ refs,
198
+ allNames,
199
+ };
200
+ session.cache.set(key, compiled);
201
+ return compiled;
202
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 装配族出口:装配会话与整篇编译的入口。
3
+ *
4
+ * 轴 = 装配会话三件——形状(`types.ts`)、会话入口(`session.ts`)、函数编译细节(`functions.ts`)。
5
+ * 函数编译细节属族内实现,不进出口。
6
+ */
7
+ export { compileStoryFile, createAssembleSession } from "./session.ts";
8
+ export type { AssembleSession, CompiledStory } from "./session.ts";
@@ -0,0 +1,207 @@
1
+ import {
2
+ compareOrdinal,
3
+ type CharacterDef,
4
+ type Story,
5
+ type StoryCommand,
6
+ } from "@lingfan-engine/core";
7
+ import {
8
+ defaultStoriesBuildTexts,
9
+ type StoriesBuildTexts,
10
+ } from "../../../shared/index.ts";
11
+ import type { DiagBag, Graph } from "../graph.ts";
12
+ import { compileFunction, partitionFunctions, tryFuncOp } from "./functions.ts";
13
+ import {
14
+ createBodyCtx,
15
+ translateStatements,
16
+ type FuncRegistry,
17
+ } from "../translate.ts";
18
+ import type {
19
+ AssembleSession,
20
+ CompiledFunction,
21
+ CompiledStory,
22
+ } from "./types.ts";
23
+
24
+ export type { AssembleSession, CompiledStory } from "./types.ts";
25
+
26
+ /**
27
+ * 会话与列编译:会话持有跨文件共享状态(函数缓存/字体表/文案表/全工程角色表),
28
+ * compileStoryFile 把单个故事文件编译成单列 Story。函数体翻译在 ./functions.ts。
29
+ */
30
+
31
+ export function createAssembleSession(
32
+ fontNames: ReadonlySet<string> = new Set(),
33
+ texts: StoriesBuildTexts = defaultStoriesBuildTexts,
34
+ characters: ReadonlyMap<string, CharacterDef> = new Map(),
35
+ ): AssembleSession {
36
+ return { cache: new Map(), fontNames, texts, characters };
37
+ }
38
+
39
+ export function compileStoryFile(
40
+ session: AssembleSession,
41
+ graph: Graph,
42
+ diags: DiagBag,
43
+ rel: string,
44
+ ): CompiledStory | undefined {
45
+ const mod = graph.records.get(rel);
46
+ const storyName = mod?.story?.node.name;
47
+ if (mod === undefined || mod.story === null || storyName === undefined)
48
+ return undefined;
49
+ const storyDecl = mod.story;
50
+ const sf = mod.sf;
51
+ const columnId = storyDecl.name;
52
+
53
+ // —— 列级字体:声明值对照字体名注册表(fail-closed,与词引用同一组诊断文案)——
54
+ let columnFont: string | undefined;
55
+ if (mod.columnFont !== null) {
56
+ const { value, node } = mod.columnFont;
57
+ if (session.fontNames.has(value)) {
58
+ columnFont = value;
59
+ } else {
60
+ diags.add(
61
+ rel,
62
+ sf,
63
+ node,
64
+ `未注册的字体名 "${value}"——Resources/Fonts 里没有对应字体文件(字体名 = 文件名去扩展名)`,
65
+ );
66
+ }
67
+ }
68
+
69
+ const registry: FuncRegistry = { refs: new Map(), names: new Map() };
70
+ const shared = {
71
+ mod,
72
+ file: rel,
73
+ diags,
74
+ funcs: registry,
75
+ stateRoot: graph.stateRoot,
76
+ fontNames: session.fontNames,
77
+ texts: session.texts,
78
+ };
79
+ const ctx = createBodyCtx(shared, { inFunction: false });
80
+ const { fns: nestedDecls, rest } = partitionFunctions(
81
+ storyDecl.node.body!.statements,
82
+ );
83
+ // 函数声明提升:嵌套名先登记进列根作用域,列体即可引用它们
84
+ for (const nestedDecl of nestedDecls) {
85
+ ctx.scopes[ctx.scopes.length - 1].set(nestedDecl.name!.text, {
86
+ kind: "function",
87
+ file: rel,
88
+ });
89
+ }
90
+ const bodyCommands = translateStatements(ctx, rest);
91
+ // 嵌套函数体继承列根作用域(列级 let/嵌套名只读下传);列体翻译先完成,声明已齐备
92
+ const nested: CompiledFunction[] = [];
93
+ for (const nestedDecl of nestedDecls) {
94
+ const compiled = compileFunction(
95
+ session,
96
+ graph,
97
+ diags,
98
+ rel,
99
+ [columnId],
100
+ nestedDecl,
101
+ ctx.scopes,
102
+ );
103
+ if (compiled === undefined) continue;
104
+ nested.push(compiled);
105
+ }
106
+
107
+ const closure = new Map<string, CompiledFunction>();
108
+ const worklist = [...registry.refs.keys()];
109
+ while (worklist.length > 0) {
110
+ const key = worklist.pop()!;
111
+ if (closure.has(key)) continue;
112
+ const sep = key.indexOf("#");
113
+ const file = key.slice(0, sep);
114
+ const fnName = key.slice(sep + 1);
115
+ const target = graph.records.get(file);
116
+ const decl = target?.functions.get(fnName);
117
+ if (target === undefined || decl === undefined) continue;
118
+ const compiled = compileFunction(session, graph, diags, file, [], decl, []);
119
+ if (compiled === undefined) continue;
120
+ closure.set(key, compiled);
121
+ worklist.push(...compiled.refs);
122
+ }
123
+
124
+ const columnNames = new Map<
125
+ string,
126
+ { file: string; line: number; col: number }
127
+ >();
128
+ const registerName = (
129
+ n: string,
130
+ pos: { file: string; line: number; col: number },
131
+ ): void => {
132
+ const prev = columnNames.get(n);
133
+ if (prev !== undefined) {
134
+ diags.add(
135
+ rel,
136
+ sf,
137
+ storyName,
138
+ `同列存在同名 func \`${n}\`:${prev.file}:${prev.line}:${prev.col} 与 ${pos.file}:${pos.line}:${pos.col}`,
139
+ );
140
+ return;
141
+ }
142
+ columnNames.set(n, pos);
143
+ };
144
+ const localGroup: CompiledFunction[] = [...nested];
145
+ const importedGroup: CompiledFunction[] = [];
146
+ for (const compiled of closure.values()) {
147
+ if (compiled.file === rel) localGroup.push(compiled);
148
+ else importedGroup.push(compiled);
149
+ }
150
+ for (const group of [localGroup, importedGroup]) {
151
+ for (const compiled of group) {
152
+ for (const [n, pos] of compiled.allNames) registerName(n, pos);
153
+ }
154
+ }
155
+
156
+ const commands: StoryCommand[] = [];
157
+ // 全工程角色表前置:模块层声明的角色进场即有(注册先于一切命令,含 func)
158
+ for (const def of [...session.characters.values()].sort((a, b) =>
159
+ compareOrdinal(a.key, b.key),
160
+ )) {
161
+ commands.push(characterCommand(def));
162
+ }
163
+ for (const group of [localGroup, importedGroup]) {
164
+ for (const compiled of [...group].sort((a, b) =>
165
+ compareOrdinal(a.name, b.name),
166
+ )) {
167
+ const op = tryFuncOp(
168
+ sf,
169
+ storyName,
170
+ diags,
171
+ compiled.name,
172
+ compiled.params,
173
+ compiled.body,
174
+ );
175
+ if (op === undefined) continue;
176
+ commands.push(op);
177
+ }
178
+ }
179
+ commands.push(...bodyCommands);
180
+
181
+ const story: Story = {
182
+ formatVersion: 1,
183
+ id: columnId,
184
+ entry: columnId,
185
+ columns: [
186
+ {
187
+ id: columnId,
188
+ kind: "flow",
189
+ ...(columnFont !== undefined ? { font: columnFont } : {}),
190
+ commands,
191
+ },
192
+ ],
193
+ };
194
+ return { columnId, sourceRelPath: rel, story };
195
+ }
196
+
197
+ /** 模块层角色声明 → character 命令(六字段全可选,有值才投影) */
198
+ function characterCommand(def: CharacterDef): StoryCommand {
199
+ const op: StoryCommand = { op: "character", key: def.key };
200
+ if (def.name !== undefined) op.name = def.name;
201
+ if (def.color !== undefined) op.color = def.color;
202
+ if (def.size !== undefined) op.size = def.size;
203
+ if (def.font !== undefined) op.font = def.font;
204
+ if (def.textColor !== undefined) op.textColor = def.textColor;
205
+ if (def.screen !== undefined) op.screen = def.screen;
206
+ return op;
207
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 装配层的公共形状:会话状态、单列编译结果与函数体编译产物。
3
+ *
4
+ * 类型放叶子:会话实现与函数体翻译都要用这些形状,声明在任一实现里都会让另一方反向依赖。
5
+ */
6
+
7
+ import type { CharacterDef, Story, StoryCommand } from "@lingfan-engine/core";
8
+ import type { StoriesBuildTexts } from "../../../shared/index.ts";
9
+
10
+ /** 会话:跨文件共享状态(函数缓存 / 字体表 / 文案表 / 全工程角色表) */
11
+ export interface AssembleSession {
12
+ cache: Map<string, CompiledFunction>;
13
+ /** 字体名注册表(编排层自 Resources/Fonts 扫描;构建期字体引用检查对照源) */
14
+ fontNames: ReadonlySet<string>;
15
+ /** 集中文案表(编译入口注入,已含缺省合并;诊断文案只从这里取) */
16
+ texts: StoriesBuildTexts;
17
+ /** 全工程角色表(编排层模块层声明收集产物):前置成每个故事 start 列头的 character 命令 */
18
+ characters: ReadonlyMap<string, CharacterDef>;
19
+ }
20
+
21
+ /** 单列编译结果 */
22
+ export interface CompiledStory {
23
+ columnId: string;
24
+ sourceRelPath: string;
25
+ story: Story;
26
+ }
27
+
28
+ /** 一次函数编译的产物:签名、体命令、被引函数名与全部嵌套名(同列重名检查用) */
29
+ export interface CompiledFunction {
30
+ key: string;
31
+ file: string;
32
+ name: string;
33
+ params: string[];
34
+ body: StoryCommand[];
35
+ refs: ReadonlySet<string>;
36
+ allNames: ReadonlyMap<string, { file: string; line: number; col: number }>;
37
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 装配族的对上游出口(本文件与 `assemble/` 目录同族,导出面与目录出口一致)。
3
+ */
4
+ export { compileStoryFile, createAssembleSession } from "./assemble/index.ts";
5
+ export type { AssembleSession, CompiledStory } from "./assemble/index.ts";
@@ -0,0 +1,208 @@
1
+ import * as ts from "typescript";
2
+ import { walkFiles, type StoriesBuildTexts } from "../../../shared/index.ts";
3
+ import type { StorySourceConfig } from "../../../config/story-config.ts";
4
+ import type { CompilerConstValues } from "../../compiler-contract.ts";
5
+ import {
6
+ DiagBag,
7
+ ROOT_TS_FILES_ALLOWED,
8
+ GEN_PREFIX,
9
+ COLUMN_FONT_DECL,
10
+ toPosix,
11
+ } from "./core.ts";
12
+ import type { Graph, ModuleRecord } from "./core.ts";
13
+ import { scanModule } from "./scan/module-scan.ts";
14
+ import { discoverStateRoot } from "./state-root.ts";
15
+ import {
16
+ scanConstStatement,
17
+ assertNpmNamesConstOnly,
18
+ } from "./scan/bindings.ts";
19
+ import { resolveImports } from "./scan/imports.ts";
20
+
21
+ /**
22
+ * 图谱构建:布局声明(config 求值产物)为必填入参——本层不再解析 config.ts
23
+ *(读取/校验统一走 const-eval/config-eval),只消费 folders/start。
24
+ */
25
+ export function buildGraph(
26
+ sourcesDir: string,
27
+ diags: DiagBag,
28
+ config: StorySourceConfig,
29
+ constValues?: CompilerConstValues,
30
+ texts?: StoriesBuildTexts,
31
+ ): Graph | null {
32
+ const files = new Map<string, string>();
33
+ for (const [rel, text] of walkFiles(sourcesDir)) {
34
+ const posix = toPosix(rel);
35
+ if (posix.startsWith(GEN_PREFIX) || !posix.endsWith(".ts")) continue;
36
+ files.set(posix, text);
37
+ }
38
+ if (!files.has("config.ts")) {
39
+ const emptySf = ts.createSourceFile(
40
+ "config.ts",
41
+ "",
42
+ ts.ScriptTarget.Latest,
43
+ true,
44
+ );
45
+ diags.addPos("config.ts", emptySf, 0, "Stories.src 根级缺少 config.ts");
46
+ return null;
47
+ }
48
+
49
+ const configSf = ts.createSourceFile(
50
+ "config.ts",
51
+ files.get("config.ts") ?? "",
52
+ ts.ScriptTarget.Latest,
53
+ true,
54
+ );
55
+ if (config.folders.length === 0) {
56
+ diags.addPos(
57
+ "config.ts",
58
+ configSf,
59
+ 0,
60
+ 'folders 为空且未包含 "*":没有编译域',
61
+ );
62
+ }
63
+ for (const rel of files.keys()) {
64
+ if (rel.includes("/") || ROOT_TS_FILES_ALLOWED.has(rel)) continue;
65
+ const sf = ts.createSourceFile(
66
+ rel,
67
+ files.get(rel) ?? "",
68
+ ts.ScriptTarget.Latest,
69
+ true,
70
+ );
71
+ diags.add(
72
+ rel,
73
+ sf,
74
+ sf,
75
+ "根级除 config.ts 外不允许 .ts 文件:故事请放入 folders 目录,纯模块请放入子目录",
76
+ );
77
+ }
78
+
79
+ const records = new Map<string, ModuleRecord>();
80
+ for (const [rel, text] of files) {
81
+ if (rel === "config.ts") continue;
82
+ records.set(rel, {
83
+ rel,
84
+ sf: ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true),
85
+ story: null,
86
+ functions: new Map(),
87
+ bindings: new Map(),
88
+ exports: new Map(),
89
+ imports: [],
90
+ consts: [],
91
+ npmNames: new Set(),
92
+ evaluated: constValues?.[rel],
93
+ columnFont: null,
94
+ });
95
+ }
96
+
97
+ for (const record of records.values()) scanModule(record, diags);
98
+ for (const record of records.values())
99
+ resolveImports(record, records, diags, true);
100
+ for (const record of records.values()) {
101
+ for (const stmt of record.consts)
102
+ scanConstStatement(stmt, record, diags, texts);
103
+ }
104
+ collectColumnFonts(records, diags);
105
+ for (const record of records.values())
106
+ resolveImports(record, records, diags, false);
107
+ for (const record of records.values())
108
+ assertNpmNamesConstOnly(record, diags, texts);
109
+
110
+ const wildcard = config.folders.includes("*");
111
+ const explicit = config.folders.filter((f) => f !== "*");
112
+ const coveredStories: string[] = [];
113
+ for (const [rel, record] of records) {
114
+ if (record.story === null) continue;
115
+ const covered = wildcard || explicit.some((f) => rel.startsWith(`${f}/`));
116
+ if (!covered) {
117
+ const node =
118
+ record.sf.statements.find((s) => ts.isExportAssignment(s)) ?? record.sf;
119
+ diags.add(
120
+ rel,
121
+ record.sf,
122
+ node,
123
+ "故事文件不在 config.folders 覆盖范围内(不会被编译为列)",
124
+ );
125
+ continue;
126
+ }
127
+ coveredStories.push(rel);
128
+ }
129
+ coveredStories.sort();
130
+
131
+ const stateRoot = discoverStateRoot(records, diags);
132
+ return { files, records, config, configSf, coveredStories, stateRoot };
133
+ }
134
+
135
+ /**
136
+ * 列级字体声明拾取:`export const columnFont = "<名>"`。
137
+ * 值走既有 const 绑定通道(字面量 / 常量运算 / 其他 const / 编译期求值内联同源),
138
+ * 这里只做归属与形状裁定;字体名是否在注册表内由汇编段对照 fontNames 报(注册表
139
+ * 在会话上,图谱层不可见)。
140
+ */
141
+ function collectColumnFonts(
142
+ records: ReadonlyMap<string, ModuleRecord>,
143
+ diags: DiagBag,
144
+ ): void {
145
+ for (const record of records.values()) {
146
+ const binding = record.exports.get(COLUMN_FONT_DECL);
147
+ if (binding === undefined) continue;
148
+ const stmt = record.consts.find((s) => {
149
+ const decl = s.declarationList.declarations[0];
150
+ return (
151
+ decl !== undefined &&
152
+ ts.isIdentifier(decl.name) &&
153
+ decl.name.text === COLUMN_FONT_DECL
154
+ );
155
+ });
156
+ const node = stmt ?? record.sf;
157
+ if (record.story === null) {
158
+ diags.add(
159
+ record.rel,
160
+ record.sf,
161
+ node,
162
+ "columnFont 只能声明在故事文件(default 导出列)的模块顶层",
163
+ );
164
+ continue;
165
+ }
166
+ if (
167
+ binding.kind !== "const" ||
168
+ typeof binding.value !== "string" ||
169
+ binding.value === ""
170
+ ) {
171
+ diags.add(
172
+ record.rel,
173
+ record.sf,
174
+ node,
175
+ "columnFont 必须折叠为非空字符串字面量(字体名 = Resources/Fonts 文件名去扩展名)",
176
+ );
177
+ continue;
178
+ }
179
+ record.columnFont = { value: binding.value, node };
180
+ }
181
+ }
182
+
183
+ export function resolveStart(graph: Graph, diags: DiagBag): string | undefined {
184
+ const candidates = graph.coveredStories.filter((rel) => {
185
+ const idx = rel.lastIndexOf("/");
186
+ return (idx === -1 ? rel : rel.slice(idx + 1)) === graph.config.start;
187
+ });
188
+ if (candidates.length === 0) {
189
+ diags.addPos(
190
+ "config.ts",
191
+ graph.configSf,
192
+ 0,
193
+ `start \`${graph.config.start}\` 没有命中任何覆盖内故事文件`,
194
+ );
195
+ return undefined;
196
+ }
197
+ if (candidates.length > 1) {
198
+ diags.addPos(
199
+ "config.ts",
200
+ graph.configSf,
201
+ 0,
202
+ `start \`${graph.config.start}\` 命中多个故事文件:${candidates.join("、")}`,
203
+ );
204
+ return undefined;
205
+ }
206
+ const record = graph.records.get(candidates[0]);
207
+ return record?.story?.name;
208
+ }