cli-calctool 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 (30) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +147 -0
  3. package/install.mjs +436 -0
  4. package/package.json +27 -0
  5. package/skills/blueprint/SKILL.md +47 -0
  6. package/skills/blueprint/install-meta.json +7 -0
  7. package/skills/blueprint/skill.json +10 -0
  8. package/skills/calctool/SKILL.md +167 -0
  9. package/skills/calctool/install-meta.json +7 -0
  10. package/skills/calctool/platform-template/README.md +74 -0
  11. package/skills/calctool/platform-template/index.html +12 -0
  12. package/skills/calctool/platform-template/package.json +26 -0
  13. package/skills/calctool/platform-template/src/App.tsx +233 -0
  14. package/skills/calctool/platform-template/src/authz.ts +103 -0
  15. package/skills/calctool/platform-template/src/engine/evaluate.ts +114 -0
  16. package/skills/calctool/platform-template/src/engine-definition.json +195 -0
  17. package/skills/calctool/platform-template/src/main.tsx +13 -0
  18. package/skills/calctool/platform-template/src/pipeline.ts +80 -0
  19. package/skills/calctool/platform-template/src/store.ts +53 -0
  20. package/skills/calctool/platform-template/tsconfig.json +15 -0
  21. package/skills/calctool/platform-template/vite.config.ts +7 -0
  22. package/skills/calctool/references/declarative-pages.md +69 -0
  23. package/skills/calctool/references/engine-meta-model.md +123 -0
  24. package/skills/calctool/references/finance-example.md +132 -0
  25. package/skills/calctool/references/formula-dsl.md +92 -0
  26. package/skills/calctool/references/import-ocr.md +72 -0
  27. package/skills/calctool/skill.json +10 -0
  28. package/skills/calctool/templates/ecommerce-ops/README.md +27 -0
  29. package/skills/calctool/templates/ecommerce-ops/domain-reference.yaml +172 -0
  30. package/sources.json +18 -0
@@ -0,0 +1,114 @@
1
+ // calctool 公式引擎:JSON AST + decimal.js 确定性求值
2
+ // 公式 DSL 规范见 references/formula-dsl.md
3
+ import Decimal from 'decimal.js'
4
+
5
+ export type FormulaNode =
6
+ | { ref: string }
7
+ | { lit: string | number }
8
+ | { op: string; args: FormulaNode[] }
9
+
10
+ export type CalcError =
11
+ | { code: 'DIV_ZERO'; nodeId: string }
12
+ | { code: 'MISSING_INPUT'; fieldId: string }
13
+ | { code: 'NON_FINITE'; nodeId: string }
14
+
15
+ export type EngineValues = Record<string, string | number | null>
16
+
17
+ /** 安全除法:除数为 0 用 fallback(默认 0),与 div 报错区分 */
18
+ function safeDiv(a: Decimal, b: Decimal, fallback = '0'): Decimal {
19
+ if (b.isZero()) return new Decimal(fallback)
20
+ return a.div(b)
21
+ }
22
+
23
+ /** 求值一个公式 AST 节点(纯函数,无副作用)
24
+ * 节点形态:{ ref: 'field' } 引用 / { lit: 1 } 字面量 / { op, args } 运算
25
+ */
26
+ export function evaluate(node: FormulaNode, values: EngineValues): Decimal {
27
+ // 引用节点(无 op,只有 ref)
28
+ if ('ref' in node && node.ref !== undefined) {
29
+ const raw = values[node.ref]
30
+ if (raw === null || raw === undefined || raw === '') {
31
+ throw { code: 'MISSING_INPUT', fieldId: node.ref } as CalcError
32
+ }
33
+ return new Decimal(String(raw))
34
+ }
35
+ // 字面量节点(无 op,只有 lit)
36
+ if ('lit' in node && node.lit !== undefined) {
37
+ return new Decimal(String(node.lit ?? 0))
38
+ }
39
+ switch (node.op) {
40
+ case 'add':
41
+ return node.args.reduce((acc, arg) => acc.plus(evaluate(arg, values)), new Decimal(0))
42
+ case 'sub': {
43
+ const [a, b] = node.args
44
+ return evaluate(a, values).minus(evaluate(b, values))
45
+ }
46
+ case 'mul':
47
+ return node.args.reduce((acc, arg) => acc.times(evaluate(arg, values)), new Decimal(1))
48
+ case 'div': {
49
+ const [a, b] = node.args
50
+ const divisor = evaluate(b, values)
51
+ if (divisor.isZero()) throw { code: 'DIV_ZERO', nodeId: 'div' } as CalcError
52
+ return evaluate(a, values).div(divisor)
53
+ }
54
+ case 'safeDivide': {
55
+ const [a, b] = node.args
56
+ return safeDiv(evaluate(a, values), evaluate(b, values))
57
+ }
58
+ case 'percentOf': {
59
+ const [a, b] = node.args
60
+ return evaluate(a, values).div(evaluate(b, values)).times(100)
61
+ }
62
+ case 'round': {
63
+ const [a] = node.args
64
+ return evaluate(a, values).toDecimalPlaces(2)
65
+ }
66
+ case 'if': {
67
+ const [cond, thenNode, elseNode] = node.args
68
+ const v = evaluate(cond, values)
69
+ return v.isZero() ? evaluate(elseNode, values) : evaluate(thenNode, values)
70
+ }
71
+ default:
72
+ throw new Error(`Unsupported operator: ${node.op}`)
73
+ }
74
+ }
75
+
76
+ /** 从 AST 提取引用的字段 key(依赖图构建用) */
77
+ export function extractRefs(node: FormulaNode | undefined): string[] {
78
+ if (!node) return []
79
+ if ('ref' in node && node.ref !== undefined) return [node.ref]
80
+ if ('lit' in node && node.lit !== undefined) return []
81
+ return (node.args ?? []).flatMap(extractRefs)
82
+ }
83
+
84
+ /** 按公式定义计算全部结果(含跨公式引用,安全除零) */
85
+ export function evaluateEngine(
86
+ formulas: Array<{ key: string; expression: FormulaNode }>,
87
+ inputs: EngineValues,
88
+ ): Record<string, string> {
89
+ const values: EngineValues = { ...inputs }
90
+ const results: Record<string, string> = {}
91
+ const visited = new Set<string>()
92
+ const stack: string[] = []
93
+
94
+ const compute = (key: string) => {
95
+ if (visited.has(key)) return
96
+ if (stack.includes(key)) throw new Error(`Cycle detected: ${[...stack, key].join(' → ')}`)
97
+ const formula = formulas.find((f) => f.key === key)
98
+ if (!formula) return
99
+ stack.push(key)
100
+ const refs = extractRefs(formula.expression)
101
+ for (const ref of refs) {
102
+ if (formulas.some((f) => f.key === ref)) compute(ref) // 跨公式引用先算
103
+ }
104
+ const result = evaluate(formula.expression, values)
105
+ const str = result.toFixed(4).replace(/\.?0+$/, '')
106
+ values[key] = str
107
+ results[key] = str
108
+ stack.pop()
109
+ visited.add(key)
110
+ }
111
+
112
+ for (const f of formulas) compute(f.key)
113
+ return results
114
+ }
@@ -0,0 +1,195 @@
1
+ {
2
+ "engineId": "ecommerce-ops-dashboard",
3
+ "name": "电商运营仪表盘",
4
+ "semanticVersion": "1.0.0",
5
+ "category": "ecommerce",
6
+ "decimalPolicy": "decimal-string",
7
+ "fields": [
8
+ {
9
+ "key": "visitors",
10
+ "label": "访客数",
11
+ "type": "integer",
12
+ "unit": "人",
13
+ "required": true
14
+ },
15
+ {
16
+ "key": "orders",
17
+ "label": "订单数",
18
+ "type": "integer",
19
+ "unit": "单",
20
+ "required": true
21
+ },
22
+ {
23
+ "key": "gmv",
24
+ "label": "GMV",
25
+ "type": "money",
26
+ "unit": "CNY",
27
+ "required": true
28
+ },
29
+ {
30
+ "key": "adSpend",
31
+ "label": "广告费",
32
+ "type": "money",
33
+ "unit": "CNY",
34
+ "required": true
35
+ },
36
+ {
37
+ "key": "customRate",
38
+ "label": "自定义转化率修正",
39
+ "type": "percent",
40
+ "unit": "%"
41
+ }
42
+ ],
43
+ "formulas": [
44
+ {
45
+ "key": "conversionRate",
46
+ "label": "转化率",
47
+ "expression": {
48
+ "op": "safeDivide",
49
+ "args": [
50
+ {
51
+ "ref": "orders"
52
+ },
53
+ {
54
+ "ref": "visitors"
55
+ }
56
+ ]
57
+ }
58
+ },
59
+ {
60
+ "key": "aov",
61
+ "label": "客单价",
62
+ "expression": {
63
+ "op": "safeDivide",
64
+ "args": [
65
+ {
66
+ "ref": "gmv"
67
+ },
68
+ {
69
+ "ref": "orders"
70
+ }
71
+ ]
72
+ }
73
+ },
74
+ {
75
+ "key": "roas",
76
+ "label": "ROAS",
77
+ "expression": {
78
+ "op": "safeDivide",
79
+ "args": [
80
+ {
81
+ "ref": "gmv"
82
+ },
83
+ {
84
+ "ref": "adSpend"
85
+ }
86
+ ]
87
+ }
88
+ },
89
+ {
90
+ "key": "gmvPerVisitor",
91
+ "label": "访客价值",
92
+ "expression": {
93
+ "op": "safeDivide",
94
+ "args": [
95
+ {
96
+ "ref": "gmv"
97
+ },
98
+ {
99
+ "ref": "visitors"
100
+ }
101
+ ]
102
+ }
103
+ }
104
+ ],
105
+ "rules": [],
106
+ "views": [],
107
+ "testSuites": [
108
+ {
109
+ "name": "基准样例",
110
+ "inputs": {
111
+ "visitors": 10000,
112
+ "orders": 300,
113
+ "gmv": 50000,
114
+ "adSpend": 12000
115
+ },
116
+ "expect": {
117
+ "conversionRate": "3",
118
+ "aov": "166.6667",
119
+ "roas": "4.1667"
120
+ }
121
+ }
122
+ ],
123
+ "pipeline": {
124
+ "engineId": "ecommerce-ops-dashboard",
125
+ "nodes": [
126
+ {
127
+ "id": "input",
128
+ "kind": "input",
129
+ "label": "录入/导入",
130
+ "inputs": [],
131
+ "outputs": [
132
+ "validate"
133
+ ]
134
+ },
135
+ {
136
+ "id": "validate",
137
+ "kind": "validate",
138
+ "label": "数据校验",
139
+ "inputs": [
140
+ "input"
141
+ ],
142
+ "outputs": [
143
+ "compute"
144
+ ]
145
+ },
146
+ {
147
+ "id": "compute",
148
+ "kind": "compute",
149
+ "label": "确定性计算",
150
+ "inputs": [
151
+ "validate"
152
+ ],
153
+ "outputs": [
154
+ "store",
155
+ "output"
156
+ ]
157
+ },
158
+ {
159
+ "id": "store",
160
+ "kind": "store",
161
+ "label": "数据存储",
162
+ "inputs": [
163
+ "compute"
164
+ ],
165
+ "outputs": [
166
+ "output"
167
+ ]
168
+ },
169
+ {
170
+ "id": "output",
171
+ "kind": "output",
172
+ "label": "指标卡/报告",
173
+ "inputs": [
174
+ "compute",
175
+ "store"
176
+ ],
177
+ "outputs": []
178
+ },
179
+ {
180
+ "id": "automate",
181
+ "kind": "automate",
182
+ "label": "自动化(可选)",
183
+ "inputs": [
184
+ "store"
185
+ ],
186
+ "outputs": [
187
+ "output"
188
+ ],
189
+ "config": {
190
+ "enabled": false
191
+ }
192
+ }
193
+ ]
194
+ }
195
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import { ConfigProvider } from 'antd'
4
+ import zhCN from 'antd/locale/zh_CN'
5
+ import App from './App'
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <ConfigProvider locale={zhCN}>
10
+ <App />
11
+ </ConfigProvider>
12
+ </React.StrictMode>,
13
+ )
@@ -0,0 +1,80 @@
1
+ // calctool 链路编排:整套工具 = 联通节点(非单页孤岛)
2
+ // 定义数据如何在节点间流动:输入 → 校验 → 计算 → 存储 → 输出 → 自动化
3
+ // 节点可增删改,链路随之更新 —— 后期改工具只动这里,不改页面
4
+
5
+ import type { FormulaNode } from './engine/evaluate'
6
+
7
+ export type PipelineNodeKind =
8
+ | 'input' // 数据输入(表单/导入)
9
+ | 'validate' // 校验(必填/类型/范围)
10
+ | 'compute' // 确定性计算(公式引擎)
11
+ | 'store' // 数据存储(持久化)
12
+ | 'output' // 输出(指标卡/报告/导出)
13
+ | 'automate' // 自动化(触发器/定时/通知)
14
+
15
+ export type PipelineNode = {
16
+ id: string
17
+ kind: PipelineNodeKind
18
+ label: string
19
+ inputs: string[] // 上游节点 id
20
+ outputs: string[] // 下游节点 id
21
+ config?: Record<string, unknown>
22
+ }
23
+
24
+ export type Pipeline = {
25
+ engineId: string
26
+ nodes: PipelineNode[]
27
+ // 数据流:input -> validate -> compute -> store -> output (+ automate)
28
+ }
29
+
30
+ /** 默认链路:整套工具的骨架(模块联通,非孤岛) */
31
+ export function defaultPipeline(engineId: string): Pipeline {
32
+ return {
33
+ engineId,
34
+ nodes: [
35
+ { id: 'input', kind: 'input', label: '录入/导入', inputs: [], outputs: ['validate'] },
36
+ { id: 'validate', kind: 'validate', label: '数据校验', inputs: ['input'], outputs: ['compute'] },
37
+ { id: 'compute', kind: 'compute', label: '确定性计算', inputs: ['validate'], outputs: ['store', 'output'] },
38
+ { id: 'store', kind: 'store', label: '数据存储', inputs: ['compute'], outputs: ['output'] },
39
+ { id: 'output', kind: 'output', label: '指标卡/报告', inputs: ['compute', 'store'], outputs: [] },
40
+ { id: 'automate', kind: 'automate', label: '自动化(可选)', inputs: ['store'], outputs: ['output'], config: { enabled: false } },
41
+ ],
42
+ }
43
+ }
44
+
45
+ export type FieldDef = {
46
+ key: string
47
+ label: string
48
+ type: 'number' | 'integer' | 'money' | 'percent' | 'text' | 'date' | 'enum'
49
+ unit?: string
50
+ required?: boolean
51
+ }
52
+
53
+ export type FormulaDef = {
54
+ key: string
55
+ label: string
56
+ expression: FormulaNode
57
+ }
58
+
59
+ export type EngineDefinition = {
60
+ engineId: string
61
+ name: string
62
+ semanticVersion: string
63
+ category: string
64
+ decimalPolicy: string
65
+ fields: FieldDef[]
66
+ formulas: FormulaDef[]
67
+ rules: unknown[]
68
+ pipeline: Pipeline
69
+ }
70
+
71
+ /** 在链路中找节点的上下游(改工具时定位影响范围) */
72
+ export function nodeNeighbors(pipeline: Pipeline, nodeId: string) {
73
+ const node = pipeline.nodes.find((n) => n.id === nodeId)
74
+ if (!node) return null
75
+ return {
76
+ node,
77
+ upstream: pipeline.nodes.filter((n) => node.inputs.includes(n.id)),
78
+ downstream: pipeline.nodes.filter((n) => n.inputs.includes(nodeId)),
79
+ }
80
+ }
@@ -0,0 +1,53 @@
1
+ // calctool 存储层:数据持久化
2
+ // 默认 localStorage(零依赖,任何环境可用);Node 全功能模式可换 better-sqlite3
3
+ export type ToolRecord = {
4
+ id: string
5
+ engineId: string
6
+ inputs: Record<string, string | number>
7
+ results: Record<string, string>
8
+ createdAt: string
9
+ updatedAt: string
10
+ }
11
+
12
+ const KEY_PREFIX = 'calctool:record:'
13
+
14
+ export class ToolStore {
15
+ constructor(private readonly engineId: string) {}
16
+
17
+ list(): ToolRecord[] {
18
+ if (typeof localStorage === 'undefined') return []
19
+ const records: ToolRecord[] = []
20
+ for (let i = 0; i < localStorage.length; i++) {
21
+ const key = localStorage.key(i)
22
+ if (key?.startsWith(`${KEY_PREFIX}${this.engineId}:`)) {
23
+ try {
24
+ records.push(JSON.parse(localStorage.getItem(key) ?? 'null'))
25
+ } catch { /* 跳过损坏记录 */ }
26
+ }
27
+ }
28
+ return records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
29
+ }
30
+
31
+ save(inputs: Record<string, string | number>, results: Record<string, string>): ToolRecord {
32
+ const now = new Date().toISOString()
33
+ const record: ToolRecord = {
34
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
35
+ engineId: this.engineId,
36
+ inputs, results, createdAt: now, updatedAt: now,
37
+ }
38
+ if (typeof localStorage !== 'undefined') {
39
+ localStorage.setItem(`${KEY_PREFIX}${this.engineId}:${record.id}`, JSON.stringify(record))
40
+ }
41
+ return record
42
+ }
43
+
44
+ clear(): void {
45
+ if (typeof localStorage === 'undefined') return
46
+ const keys: string[] = []
47
+ for (let i = 0; i < localStorage.length; i++) {
48
+ const key = localStorage.key(i)
49
+ if (key?.startsWith(`${KEY_PREFIX}${this.engineId}:`)) keys.push(key)
50
+ }
51
+ keys.forEach((k) => localStorage.removeItem(k))
52
+ }
53
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "esModuleInterop": true,
11
+ "noEmit": true,
12
+ "types": ["vite/client"]
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173, host: true },
7
+ })
@@ -0,0 +1,69 @@
1
+ # 声明式页面规格系统
2
+
3
+ 把"信息型页面"从代码降级为**数据声明**,用通用渲染器渲染几十个页面,避免复制粘贴页面结构。这是生成工具页面的核心机制。
4
+
5
+ ## 1. 规格 Schema
6
+
7
+ ```ts
8
+ interface ApplicationPageSpec {
9
+ title: string;
10
+ description?: string;
11
+ tabs?: TabSpec[];
12
+ actions?: ActionSpec[]; // 页头操作(default|primary|danger)
13
+ sections?: SectionSpec[]; // 内容分区
14
+ }
15
+
16
+ interface SectionSpec {
17
+ layout: 'rows' | 'grid' | 'table' | 'steps';
18
+ rows?: RowSpec[]; // 标签/值/说明/状态/行内操作
19
+ metrics?: MetricSpec[]; // 指标卡
20
+ columns?: ColumnSpec[];
21
+ data?: (string | number)[][]; // 表格数据
22
+ empty?: EmptyStateSpec; // 空状态(标题/说明/操作)
23
+ }
24
+ ```
25
+
26
+ ## 2. 注册与路由匹配
27
+
28
+ - 规格文件按域拆分(core/settings/management/finance/studio/supplementary),每份是 `Record<string, ApplicationPageSpec>` + 路由映射
29
+ - 支持参数化路由:`/tools/:toolId/runs/:runId`、`/automations/:automationId/edit`
30
+ - 匹配算法:按路径段数**从长到短**排序 → 逐段精确匹配(`:` 前缀为参数位)→ 命中后提取 params → `encodeURIComponent` 回填规格内 `href`
31
+
32
+ ## 3. 渲染器
33
+
34
+ `ApplicationPageSpec → 通用构建块组件树`:
35
+
36
+ ```
37
+ PageShell(内容 768px)
38
+ → PageHeader(标题/说明/操作)
39
+ → SectionCard × N(分区)
40
+ → 按 layout 分发:
41
+ rows → SettingRow 行
42
+ grid → 网格卡
43
+ table → 表格
44
+ steps → 步骤条
45
+ empty → EmptyState
46
+ ```
47
+
48
+ - 无 `href` 的操作自动 `disabled`——保证"未接入"按钮不产生假交互
49
+ - 所有构建块来自共享组件(PageShell/PageHeader/SectionCard/SettingRow/EmptyState/StatGrid/PageList/Toolbar),页面只提供数据
50
+ - 单文件 ≤ 1000 行(审计脚本强制),接近上限必须拆分
51
+
52
+ ## 4. 生成工具时的页面映射
53
+
54
+ | 工具模块 | 页面规格 |
55
+ |---|---|
56
+ | 录入表单 | 字段目录 → 自动生成 rows/grid 表单(含必填/类型/单位) |
57
+ | 指标卡 | 公式结果 → metrics 数组(StatGrid) |
58
+ | 专题工具 | 每个专题 → 独立 SectionCard(增值税/盈亏平衡/定价/利润率优化) |
59
+ | 历史记录 | table 布局 + 分页 |
60
+ | 诊断报告 | 指标卡 + 表格 + 诊断结论 + 建议 |
61
+ | 空状态 | 无数据时 EmptyState(不虚构装饰图标) |
62
+
63
+ ## 5. 硬约束(从工具项目继承)
64
+
65
+ - 正文内容容器统一 768px(与 Composer 同宽),窄屏等比收窄,禁止按页面类型自由设宽
66
+ - 禁内联 style、禁硬编码颜色、禁阴影
67
+ - 图标统一 Lucide、14px、描边 2
68
+ - 空页面不显示装饰图标,不虚构状态/数据/提示/按钮
69
+ - 能拆成组件的内容禁止堆积在页面文件中
@@ -0,0 +1,123 @@
1
+ # 引擎元模型(Engine Meta-Model)
2
+
3
+ 一个可发布、可运行、可验收的"计算工具"= 一份**引擎定义**。本文件规定引擎定义的结构与约束。
4
+
5
+ ## 1. 顶级对象
6
+
7
+ ```ts
8
+ interface EngineDefinition {
9
+ id: string; // kebab-case 稳定机器标识
10
+ name: string; // 中文显示名,可改
11
+ semanticVersion: string; // 语义版本,发布后不可原地修改
12
+ status: 'draft' | 'review' | 'published' | 'retired';
13
+ compatibilityProfile?: string; // 如 legacy-compatible(复现旧口径)
14
+ decimalPolicy: 'decimal-string' | 'source-compatible-float';
15
+ inputSchema: JsonSchema; // 输入校验
16
+ fields: FieldDefinition[]; // 字段目录
17
+ tables: TableDefinition[]; // 表格定义(如成本结构表)
18
+ dimensions: DimensionDefinition[]; // 维度(公司/部门/周期)
19
+ entities: EntitySchemaDefinition[]; // 实体
20
+ relations: RelationDefinition[]; // 关系
21
+ choiceSources: ChoiceSourceDefinition[];
22
+ interactionRules: InteractionRuleDefinition[];
23
+ formulas: FormulaDefinition[]; // 公式图
24
+ rules: RuleDefinition[]; // 规则包(阈值/评分/分级)
25
+ views: ViewDefinition[]; // 视图
26
+ documents: MarkdownDocumentDefinition[]; // Markdown 说明/报告
27
+ approvalPolicyRefs: string[];
28
+ importProfiles: ImportProfile[]; // 导入映射
29
+ reports: ReportDefinition[]; // 报告模板
30
+ outputs: OutputContract[]; // 输出契约(跨引擎引用用)
31
+ permissions: EnginePermissionPolicy;
32
+ testSuites: EngineTestSuite[]; // 确定性测试
33
+ migrationFrom?: EngineMigration[];
34
+ }
35
+ ```
36
+
37
+ **铁律**:
38
+ - `id` 与字段 `key` 是稳定机器标识;中文标签可以改,ID 不改
39
+ - 所有引用用 ID,不用页面标题或表格坐标作主键
40
+ - 发布版本不可原地修改;运行记录绑定精确的 `engineVersionId`
41
+ - 跨引擎只允许引用对方声明的输出契约
42
+
43
+ ## 2. 字段类型
44
+
45
+ ```text
46
+ number, money, percent, integer, text, richText, boolean,
47
+ date, dateTime, enum, singleSelect, multiSelect, dimensionRef,
48
+ entityRef, file, image, object, repeatGroup, table, reference, derived
49
+ ```
50
+
51
+ 每个数值字段至少含:
52
+ - `key`(稳定 ID)、`label`(中文标签)、`type`
53
+ - `required` / `defaultValue`
54
+ - `precision` / `rounding`(金额、比例、整数分别定义)
55
+ - `unit`(如 CNY、CNY/person、%)
56
+ - 枚举字段含 `choiceSourceRef`
57
+
58
+ ## 3. 公式定义
59
+
60
+ ```ts
61
+ interface FormulaDefinition {
62
+ key: string;
63
+ label: string;
64
+ type: 'derived' | 'score' | 'aggregate' | 'lookup';
65
+ expression: FormulaAst; // 编译后的 AST(见 formula-dsl.md)
66
+ source?: string; // 可读字符串(仅编辑用,保存必须 AST)
67
+ outputUnit?: string;
68
+ description?: string;
69
+ }
70
+ ```
71
+
72
+ ## 4. 规则包(Rule Pack)
73
+
74
+ ```ts
75
+ interface RuleDefinition {
76
+ id: string;
77
+ name: string;
78
+ kind: 'threshold' | 'scoring' | 'grading' | 'validation';
79
+ // 阈值:区间 → 级别
80
+ // 评分:指标值 → 0-100 分(可曲线/分段)
81
+ // 分级:总分 → 健康等级(如 优/良/中/差)
82
+ inputs: string[]; // 引用字段/公式 key
83
+ evaluation: RuleAst; // 条件 AST
84
+ }
85
+ ```
86
+
87
+ ## 5. 导入 Profile
88
+
89
+ ```ts
90
+ interface ImportProfile {
91
+ id: string;
92
+ kind: 'excel' | 'ocr' | 'manual';
93
+ sourceFormat: string; // Excel 列 / OCR 字段布局
94
+ mapping: { source: string; targetField: string; transform?: string }[];
95
+ verification: 'draft' | 'auto'; // 自动导入先进草稿
96
+ }
97
+ ```
98
+
99
+ ## 6. 报告模板
100
+
101
+ ```ts
102
+ interface ReportDefinition {
103
+ id: string;
104
+ name: string;
105
+ sections: ReportSection[];
106
+ // 指标卡 / 表格 / 诊断结论 / 历史分位 / 建议
107
+ }
108
+ ```
109
+
110
+ ## 7. 版本与迁移
111
+
112
+ - `1.x legacy-compatible`:复现原工具结果(保留旧公式、边界、取整)
113
+ - `2.x corrected`:修正数据质量问题(通过显式迁移发布,绝不静默改变旧运行)
114
+ - 任何公式/字段/阈值/导入映射/报告变化 → 新版本
115
+
116
+ ## 8. 引擎状态机(能力接入)
117
+
118
+ ```text
119
+ planned → not_installed → disconnected → configuring
120
+ → pending_verification → connected → disabled | unavailable
121
+ ```
122
+
123
+ 健康状态 `healthy/degraded/failed/unknown` **只在真实连接与探针后出现**;客户端不能提交健康状态。