@entrorove/skill 0.1.23 → 0.1.24

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 (155) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CHANGELOG.md +5 -0
  3. package/SKILL.md +12 -4
  4. package/archguard-runtime-build.mjs +15 -0
  5. package/blueprint-criterion-runner.mjs +110 -0
  6. package/blueprint-criterion-wire.mjs +78 -0
  7. package/calctool-runtime-build.mjs +59 -0
  8. package/calctool-runtime-entry.mjs +92 -0
  9. package/cli.mjs +17 -82
  10. package/local-approved-plan.mjs +74 -15
  11. package/local-archguard-ast.mjs +371 -0
  12. package/local-archguard-checkpoint.mjs +123 -0
  13. package/local-archguard-components.mjs +177 -0
  14. package/local-archguard-contract.mjs +109 -0
  15. package/local-archguard-contracts.mjs +42 -0
  16. package/local-archguard-evaluate.mjs +120 -0
  17. package/local-archguard-language-ast.mjs +165 -0
  18. package/local-archguard-mutation-fence.mjs +28 -0
  19. package/local-archguard-operations.mjs +25 -0
  20. package/local-archguard-scope.mjs +64 -0
  21. package/local-archguard-signing.mjs +52 -0
  22. package/local-archguard-snapshot.mjs +126 -0
  23. package/local-archguard-state.mjs +200 -0
  24. package/local-archguard.mjs +74 -0
  25. package/local-budget-git-attributes.mjs +49 -0
  26. package/local-budget-git-loader.mjs +61 -0
  27. package/local-budget-git-native-attributes.mjs +81 -0
  28. package/local-budget-git-native-darwin.mjs +104 -0
  29. package/local-budget-git-native-linux.mjs +93 -0
  30. package/local-budget-git-native-reads.mjs +88 -0
  31. package/local-budget-git-native-request.mjs +53 -0
  32. package/local-budget-git-native-runtime.mjs +18 -0
  33. package/local-budget-git-native.c +313 -0
  34. package/local-budget-git-native.mjs +109 -0
  35. package/local-budget-git-outcome.mjs +33 -0
  36. package/local-budget-git-scratch-loader.mjs +158 -0
  37. package/local-budget-git-scratch.mjs +112 -0
  38. package/local-budget-git-system-attributes.mjs +44 -0
  39. package/local-budget-git.mjs +92 -0
  40. package/local-budget-grants.mjs +82 -0
  41. package/local-budget-handoff-completion.mjs +70 -0
  42. package/local-budget-handoff-contracts.mjs +53 -0
  43. package/local-budget-handoff.mjs +229 -0
  44. package/local-budget-lock.mjs +55 -0
  45. package/local-budget-reads.mjs +135 -0
  46. package/local-budget-renewal.mjs +86 -34
  47. package/local-budget-source-bindings.mjs +51 -0
  48. package/local-budget-store.mjs +131 -0
  49. package/local-budget-transition.mjs +46 -0
  50. package/local-budget.mjs +185 -0
  51. package/local-calctool-cli.mjs +22 -0
  52. package/local-calctool-contracts.mjs +42 -0
  53. package/local-calctool-runtime.mjs +10 -0
  54. package/local-calctool-storage.mjs +101 -0
  55. package/local-calctool.mjs +103 -0
  56. package/local-cli.mjs +49 -16
  57. package/local-contract-types.mjs +46 -0
  58. package/local-contracts.mjs +124 -0
  59. package/local-credential.mjs +101 -0
  60. package/local-decimal.mjs +85 -0
  61. package/local-evidence-verification.mjs +199 -0
  62. package/local-execution.mjs +115 -304
  63. package/local-final-gate16-bundle.mjs +21 -0
  64. package/local-final-gate16-checks.mjs +88 -0
  65. package/local-final-gate16-contract.mjs +15 -0
  66. package/local-final-gate16-plan.mjs +66 -0
  67. package/local-final-gate16-proof.mjs +75 -0
  68. package/local-final-gate16-release.mjs +17 -0
  69. package/local-final-gate16-state.mjs +70 -0
  70. package/local-final-gate16-worker.mjs +19 -0
  71. package/local-final-gate16.mjs +137 -0
  72. package/local-formula-ast.mjs +97 -0
  73. package/local-formula-engine.mjs +111 -0
  74. package/local-formula-tests.mjs +61 -0
  75. package/local-formula.mjs +25 -125
  76. package/local-integrity.mjs +22 -41
  77. package/local-invocations.mjs +107 -0
  78. package/local-ledger.mjs +2 -7
  79. package/local-merge-archguard-verification.d.mts +44 -0
  80. package/local-merge-archguard-verification.mjs +240 -0
  81. package/local-merge-archguard.mjs +138 -0
  82. package/local-merge-artifacts.mjs +86 -0
  83. package/local-merge-authority.mjs +89 -0
  84. package/local-merge-contracts.mjs +56 -0
  85. package/local-merge-controller.mjs +170 -0
  86. package/local-merge-head.mjs +56 -0
  87. package/local-merge-journal.mjs +77 -0
  88. package/local-merge-mutation.mjs +97 -0
  89. package/local-merge-rollback.mjs +126 -0
  90. package/local-merge-snapshot-rollback.mjs +26 -0
  91. package/local-merge-storage.mjs +103 -0
  92. package/local-merge-targets.mjs +47 -0
  93. package/local-merge-transaction.mjs +133 -0
  94. package/local-mutation-audit.mjs +34 -0
  95. package/local-mutation-authority.mjs +96 -0
  96. package/local-mutation-batch-receipt.mjs +84 -0
  97. package/local-mutation-batch.mjs +143 -0
  98. package/local-mutation-binding.mjs +86 -0
  99. package/local-mutation-files.mjs +245 -0
  100. package/local-mutation-pass-store.mjs +63 -0
  101. package/local-mutation-snapshot.mjs +200 -0
  102. package/local-mutation-wire.mjs +89 -0
  103. package/local-probe.mjs +133 -11
  104. package/local-scope-candidate.mjs +28 -0
  105. package/local-scope-context.mjs +28 -0
  106. package/local-scope-files.mjs +106 -0
  107. package/local-scope-git-attributes.mjs +200 -0
  108. package/local-scope-git-comparison.mjs +8 -0
  109. package/local-scope-git-history.mjs +72 -0
  110. package/local-scope-git-reads.mjs +66 -0
  111. package/local-scope-git.mjs +124 -0
  112. package/local-scope-repository.mjs +249 -0
  113. package/local-scope-signature.mjs +70 -0
  114. package/local-scope-snapshot.mjs +39 -0
  115. package/local-scope-state.mjs +204 -0
  116. package/local-scope-workspaces.mjs +83 -0
  117. package/local-swarm-authority.mjs +143 -0
  118. package/local-swarm-baseline.mjs +32 -0
  119. package/local-swarm-completion.mjs +102 -0
  120. package/local-swarm-contracts.mjs +47 -0
  121. package/local-swarm-mutation.mjs +142 -0
  122. package/local-swarm-priority.mjs +237 -0
  123. package/local-swarm-state.mjs +102 -0
  124. package/local-swarm-storage.mjs +135 -0
  125. package/local-swarm.mjs +179 -0
  126. package/local-task-handoff-completion-wire.mjs +71 -0
  127. package/local-task-handoff-contracts.mjs +66 -0
  128. package/local-task-handoff-fence.mjs +478 -0
  129. package/local-task-handoff-observation.mjs +38 -0
  130. package/local-task-handoff-overlay-wire.mjs +94 -0
  131. package/local-task-handoff-read-authority.mjs +236 -0
  132. package/local-task-handoff-storage.mjs +73 -0
  133. package/local-task-handoff-write-archive.mjs +80 -0
  134. package/local-task-handoff-write-constraints.mjs +66 -0
  135. package/local-task-handoff-write-receipts.mjs +99 -0
  136. package/local-task-handoff-write-wire.mjs +99 -0
  137. package/local-task-routing.mjs +202 -22
  138. package/manifest.json +1 -1
  139. package/package.json +153 -3
  140. package/release-state.json +153 -17
  141. package/release.mjs +238 -4
  142. package/releases.json +6 -0
  143. package/remote-client.mjs +127 -36
  144. package/remote-transport.mjs +80 -0
  145. package/review-client.mjs +38 -0
  146. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  147. package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
  148. package/runtime/git-native-darwin-v1.mjs +5 -0
  149. package/runtime/git-native-linux-v1.mjs +5 -0
  150. package/task-message-cli.mjs +47 -0
  151. package/task-message-entry.mjs +159 -0
  152. package/workflow-local-client.mjs +35 -0
  153. package/workflow-local-contracts.mjs +58 -0
  154. package/workflow-local-request.mjs +53 -0
  155. package/workflow-local-runner.mjs +350 -0
@@ -0,0 +1,1887 @@
1
+ // Generated by calctool-runtime-build.mjs from the pinned Calctool snapshot and first-party formula modules.
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ const calctoolCore = (() => {
6
+ // calctool 确定性引擎:协议常量、校验规则与蜂群协同构件(运行时主文件 calctool-runtime.mjs 引用)
7
+ const REQUEST_SCHEMA = "calctool.skill.request/1.0";
8
+ const RESPONSE_SCHEMA = "calctool.skill.response/1.0";
9
+ const ERROR_SCHEMA = "calctool.skill.error/1.0";
10
+ const ENGINE_SCHEMA = "engine.spec/1";
11
+
12
+ // 外部端点允许列表(安全审计声明)
13
+ const ALLOWED_EXTERNAL_ENDPOINTS = {
14
+ blueprint: "https://entrorove.com/wvz6zmRWmX",
15
+ swarm: "https://entrorove.com/zj7fTPVh4p",
16
+ };
17
+ const COORDINATOR_SCHEMA = "calctool.coordinator.run-plan/1.0";
18
+ const COMPILER_NAME = "calctool";
19
+ const COMPILER_VERSION = "v7.0.43";
20
+ const DEFAULT_MAX_RESPONSE_BYTES = 200_000;
21
+
22
+ const PURE_OPERATIONS = new Set(["capabilities", "help", "intake", "validate", "compile-inline"]);
23
+
24
+ // 蜂群任务分区类型(calctool 专属)
25
+ const CALCTOOL_PARTITION_KINDS = new Set([
26
+ "field", "formula", "rule", "import", "report", "page", "contract", "acceptance",
27
+ "research", "custom",
28
+ ]);
29
+ // 复核模式(与 blueprint 对齐)
30
+ const CALCTOOL_REVIEW_MODES = new Set(["none", "required", "independent"]);
31
+
32
+ const ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
33
+ const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
34
+
35
+ const INTAKE_QUESTIONS = Object.freeze([
36
+ {
37
+ id: "goal",
38
+ prompt: "Describe what the tool must let users do: which domain, what inputs, what outputs. What must never happen?",
39
+ required: true,
40
+ example: "财务经营健康诊断:输入利润表 50 个字段,输出十大指标评分与健康报告;绝不虚构后端数据。",
41
+ },
42
+ {
43
+ id: "inputs",
44
+ prompt: "List the user-entered fields (name, type, unit). Include any custom indicators the user wants to define.",
45
+ required: true,
46
+ example: "收入(money,CNY)、货品成本(money,CNY)、正式员工数(integer,人);用户可自定义指标。",
47
+ },
48
+ {
49
+ id: "formulas",
50
+ prompt: "Describe the calculation logic: which derived metrics, formulas, scoring rules, or grading thresholds.",
51
+ required: true,
52
+ example: "运营毛利 = 收入 - 货品成本;健康分 = Σ(指标分×权重),权重 30%/17%/15%/…;除零回退 0。",
53
+ },
54
+ {
55
+ id: "input-method",
56
+ prompt: "How do users get data in: manual entry, Excel upload, image/PDF OCR, or a combination?",
57
+ required: false,
58
+ example: "Excel 上传 + 手工录入;Excel 原生解析,截图走 OCR 草稿确认。",
59
+ },
60
+ {
61
+ id: "output",
62
+ prompt: "What output forms are needed: metric cards, tables, diagnostic report, history, export?",
63
+ required: false,
64
+ example: "指标卡 + 诊断报告(HTML) + 历史记录。",
65
+ },
66
+ {
67
+ id: "constraints",
68
+ prompt: "State hard constraints: precision/rounding, language, offline/online, storage, forbidden behaviors.",
69
+ required: false,
70
+ example: "金额保留 2 位小数;中文界面;无账号也可用;禁止执行任意 JavaScript。",
71
+ },
72
+ ]);
73
+
74
+ const INTAKE_INSTRUCTION = "Ask the user these questions one at a time and wait for each answer. Do not compile the engine until all required questions are answered.";
75
+
76
+ const SKILL_NAME = "calctool";
77
+ const SKILL_DESCRIPTION = "按需生成「万能计算工具」:输入领域需求,通过提问明确指标/公式/输入方式/输出形式,生成可执行、可验证、可发布的引擎定义与在线计算工具。";
78
+
79
+ const OPERATION_CATALOG = Object.freeze([
80
+ {
81
+ operation: "capabilities",
82
+ summary: "Discover skill capabilities, the operation list, per-operation inputs, and the recommended next step.",
83
+ input: {},
84
+ example: { schemaVersion: REQUEST_SCHEMA, requestId: "demo-1", operation: "capabilities", input: {} },
85
+ },
86
+ {
87
+ operation: "help",
88
+ summary: "Return the usage guide, operation catalog, and request examples.",
89
+ input: {},
90
+ example: { schemaVersion: REQUEST_SCHEMA, requestId: "demo-2", operation: "help", input: {} },
91
+ },
92
+ {
93
+ operation: "intake",
94
+ summary: "Return the questions the IDE must ask the user before generating the engine.",
95
+ input: {},
96
+ example: { schemaVersion: REQUEST_SCHEMA, requestId: "demo-3", operation: "intake", input: {} },
97
+ },
98
+ {
99
+ operation: "validate",
100
+ summary: "Deterministically validate an engine definition without generating artifacts.",
101
+ input: { engine: "An engine definition object conforming to engine.spec/1." },
102
+ example: { schemaVersion: REQUEST_SCHEMA, requestId: "demo-4", operation: "validate", input: { engine: {} } },
103
+ },
104
+ {
105
+ operation: "compile-inline",
106
+ summary: "Validate and compile an engine definition from the collected requirements, returning the generated definition inline.",
107
+ input: { requirements: "Collected intake answers (goal/inputs/formulas/inputMethod/output/constraints)." },
108
+ example: { schemaVersion: REQUEST_SCHEMA, requestId: "demo-5", operation: "compile-inline", input: { requirements: {} } },
109
+ },
110
+ ]);
111
+ const LOCAL_RUNNER_OPERATIONS = new Set([
112
+ "run", "compile", "generate", "verify", "estimate", "impact", "status", "inventory", "purge",
113
+ "brain-handshake", "brain-invoke", "brain-events", "brain-cancel", "brain-complete", "brain-resume", "brain-status",
114
+ "probe-env", "adapt-config", "intake-round", "compile-tool", "blueprint-orchestrate", "auto-pipeline",
115
+ "final-gate", "swarm-orchestrate",
116
+ ]);
117
+
118
+ // ---------- 工具函数 ----------
119
+ function text(value) { return String(value ?? ""); }
120
+
121
+ function okResponse(requestId, payload) {
122
+ return { schemaVersion: RESPONSE_SCHEMA, requestId, status: "succeeded", ...payload };
123
+ }
124
+
125
+ function blockedResponse(requestId, request, findings) {
126
+ return {
127
+ schemaVersion: RESPONSE_SCHEMA,
128
+ requestId,
129
+ status: "blocked",
130
+ brainMode: null,
131
+ requestedBrainMode: request?.requestedBrainMode ?? "ide",
132
+ brainUsed: false,
133
+ revision: null,
134
+ validation: { valid: false, guarantee: "blocked", findings },
135
+ };
136
+ }
137
+
138
+ function finding(severity, ruleId, entityRef, message, evidence = {}) {
139
+ return { severity, ruleId, entityRef, message, evidence };
140
+ }
141
+
142
+ // ---------- 引擎定义校验(engine.spec/1) ----------
143
+ function validateEngine(engine) {
144
+ const findings = [];
145
+ if (engine === null || typeof engine !== "object" || Array.isArray(engine)) {
146
+ return [finding("P0", "ENGINE_OBJECT", "engine", "engine must be an object.")];
147
+ }
148
+ if (engine.schemaVersion !== ENGINE_SCHEMA) {
149
+ findings.push(finding("P0", "ENGINE_SCHEMA_VERSION", "engine.schemaVersion", `Expected ${ENGINE_SCHEMA}.`));
150
+ }
151
+ if (!text(engine.engineId)) findings.push(finding("P0", "ENGINE_REQUIRED_FIELD", "engine.engineId", "engineId is required."));
152
+ if (!text(engine.name)) findings.push(finding("P0", "ENGINE_REQUIRED_FIELD", "engine.name", "name is required."));
153
+ if (!text(engine.semanticVersion)) findings.push(finding("P0", "ENGINE_REQUIRED_FIELD", "engine.semanticVersion", "semanticVersion is required."));
154
+ if (!Array.isArray(engine.fields)) {
155
+ findings.push(finding("P0", "ENGINE_REQUIRED_ARRAY", "engine.fields", "fields must be an array."));
156
+ } else {
157
+ const keys = new Set();
158
+ for (const [i, f] of engine.fields.entries()) {
159
+ const ref = `engine.fields[${i}]`;
160
+ if (!text(f?.key)) findings.push(finding("P0", "FIELD_REQUIRED_KEY", ref, "field key is required."));
161
+ else if (keys.has(f.key)) findings.push(finding("P0", "FIELD_DUPLICATE_KEY", ref, `duplicate field key ${f.key}.`));
162
+ else keys.add(f.key);
163
+ if (!f?.type) findings.push(finding("P0", "FIELD_REQUIRED_TYPE", ref, "field type is required."));
164
+ if (f?.unit === undefined && ["number", "money", "percent", "integer"].includes(f?.type)) {
165
+ findings.push(finding("P1", "FIELD_UNIT_MISSING", ref, "numeric field should declare a unit."));
166
+ }
167
+ }
168
+ }
169
+ if (engine.formulas !== undefined) {
170
+ if (!Array.isArray(engine.formulas)) {
171
+ findings.push(finding("P0", "ENGINE_REQUIRED_ARRAY", "engine.formulas", "formulas must be an array."));
172
+ } else {
173
+ const fieldKeys = new Set((engine.fields ?? []).map((f) => f?.key));
174
+ const formulaKeys = new Set((engine.formulas ?? []).map((f) => f?.key));
175
+ for (const [i, fm] of engine.formulas.entries()) {
176
+ const ref = `engine.formulas[${i}]`;
177
+ if (!text(fm?.key)) findings.push(finding("P0", "FORMULA_REQUIRED_KEY", ref, "formula key is required."));
178
+ if (!fm?.expression) findings.push(finding("P0", "FORMULA_REQUIRED_EXPRESSION", ref, "formula expression (AST or text) is required."));
179
+ const refs = extractRefs(fm?.expression);
180
+ for (const r of refs) {
181
+ // 引用可以指向字段(field-catalog)或公式输出(formula key),二者皆合法
182
+ if (!fieldKeys.has(r) && !formulaKeys.has(r)) {
183
+ findings.push(finding("P1", "FORMULA_REF_MISSING", `${ref}.expression`, `formula references missing field or formula ${r}.`));
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ if (!Array.isArray(engine.rules)) {
190
+ findings.push(finding("P0", "ENGINE_REQUIRED_ARRAY", "engine.rules", "rules must be an array."));
191
+ }
192
+ if (!Array.isArray(engine.views)) {
193
+ findings.push(finding("P0", "ENGINE_REQUIRED_ARRAY", "engine.views", "views must be an array."));
194
+ }
195
+ if (!Array.isArray(engine.testSuites)) {
196
+ findings.push(finding("P0", "ENGINE_REQUIRED_ARRAY", "engine.testSuites", "testSuites must be an array."));
197
+ }
198
+ if (!Array.isArray(engine.importProfiles)) {
199
+ findings.push(finding("P1", "ENGINE_RECOMMENDED_ARRAY", "engine.importProfiles", "importProfiles should be an array."));
200
+ }
201
+ return findings;
202
+ }
203
+
204
+ function extractRefs(expression) {
205
+ const refs = [];
206
+ if (!expression || typeof expression !== "object") return refs;
207
+ const walk = (node) => {
208
+ if (!node || typeof node !== "object") return;
209
+ if (typeof node.ref === "string") refs.push(node.ref);
210
+ for (const key of ["args", "then", "else", "cases"]) {
211
+ const v = node[key];
212
+ if (Array.isArray(v)) v.forEach(walk);
213
+ else if (v && typeof v === "object") walk(v);
214
+ }
215
+ };
216
+ walk(expression);
217
+ return refs;
218
+ }
219
+
220
+ // ---------- 确定性公式求值(final-gate 测试智能体用;Decimal 精度,与平台模板 evaluate.ts 对齐) ----------
221
+ // 自包含 Decimal 字符串算术(无外部依赖,远程可执行);最多保留 28 位小数。
222
+ const DECIMAL_SCALE = 28;
223
+ const DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
224
+
225
+ function powerOfTen(exponent) {
226
+ if (!Number.isInteger(exponent) || exponent < 0) throw new Error(`Invalid decimal exponent: ${exponent}`);
227
+ return 10n ** BigInt(exponent);
228
+ }
229
+
230
+ function normalizedParts(coefficient, scale) {
231
+ let nextCoefficient = coefficient;
232
+ let nextScale = scale;
233
+ while (nextScale > 0 && nextCoefficient % 10n === 0n) {
234
+ nextCoefficient /= 10n;
235
+ nextScale -= 1;
236
+ }
237
+ if (nextCoefficient === 0n) return { coefficient: 0n, scale: 0 };
238
+ return { coefficient: nextCoefficient, scale: nextScale };
239
+ }
240
+
241
+ function roundedCoefficient(coefficient, fromScale, toScale) {
242
+ if (fromScale <= toScale) return coefficient * powerOfTen(toScale - fromScale);
243
+ const divisor = powerOfTen(fromScale - toScale);
244
+ let quotient = coefficient / divisor;
245
+ const remainder = coefficient % divisor;
246
+ if ((remainder < 0n ? -remainder : remainder) * 2n >= divisor) {
247
+ quotient += coefficient < 0n ? -1n : 1n;
248
+ }
249
+ return quotient;
250
+ }
251
+
252
+ /**
253
+ * 自包含 Decimal 精度算术(字符串实现,无外部依赖)。
254
+ * 满足 final-gate 远程求值:纯计算、无 I/O,服务端完全可执行。
255
+ */
256
+ function decimalFrom(value) {
257
+ if (value instanceof DecimalStr) return value;
258
+ const s = String(value ?? "0").trim();
259
+ if (s === "" || s === "NaN" || s === "Infinity" || s === "-Infinity") {
260
+ throw new Error(`Invalid decimal: ${String(value)}`);
261
+ }
262
+ return new DecimalStr(s);
263
+ }
264
+
265
+ class DecimalStr {
266
+ constructor(s) {
267
+ const source = String(s).trim();
268
+ if (!DECIMAL_LITERAL.test(source)) throw new Error(`Invalid decimal: ${source}`);
269
+ const negative = source.startsWith("-");
270
+ const unsigned = source.replace(/^[+-]/, "");
271
+ const [integerPart = "0", fractionalPart = ""] = unsigned.split(".");
272
+ const digits = `${integerPart || "0"}${fractionalPart}`.replace(/^0+(?=\d)/, "") || "0";
273
+ const rawCoefficient = BigInt(digits) * (negative ? -1n : 1n);
274
+ const boundedCoefficient = fractionalPart.length > DECIMAL_SCALE
275
+ ? roundedCoefficient(rawCoefficient, fractionalPart.length, DECIMAL_SCALE)
276
+ : rawCoefficient;
277
+ const boundedScale = Math.min(fractionalPart.length, DECIMAL_SCALE);
278
+ const normalized = normalizedParts(boundedCoefficient, boundedScale);
279
+ this.coefficient = normalized.coefficient;
280
+ this.scale = normalized.scale;
281
+ this.s = DecimalStr.format(this.coefficient, this.scale);
282
+ }
283
+ static from(value) { return decimalFrom(value); }
284
+ static fromParts(coefficient, scale) {
285
+ const boundedCoefficient = scale > DECIMAL_SCALE
286
+ ? roundedCoefficient(coefficient, scale, DECIMAL_SCALE)
287
+ : coefficient;
288
+ const normalized = normalizedParts(boundedCoefficient, Math.min(scale, DECIMAL_SCALE));
289
+ return new DecimalStr(DecimalStr.format(normalized.coefficient, normalized.scale));
290
+ }
291
+ static format(coefficient, scale) {
292
+ const negative = coefficient < 0n;
293
+ const digits = (negative ? -coefficient : coefficient).toString().padStart(scale + 1, "0");
294
+ const value = scale === 0
295
+ ? digits
296
+ : `${digits.slice(0, -scale)}.${digits.slice(-scale)}`;
297
+ return negative && coefficient !== 0n ? `-${value}` : value;
298
+ }
299
+ isZero() { return this.coefficient === 0n; }
300
+ isNegative() { return this.coefficient < 0n; }
301
+ plus(other) {
302
+ const b = decimalFrom(other);
303
+ const scale = Math.max(this.scale, b.scale);
304
+ const left = this.coefficient * powerOfTen(scale - this.scale);
305
+ const right = b.coefficient * powerOfTen(scale - b.scale);
306
+ return DecimalStr.fromParts(left + right, scale);
307
+ }
308
+ minus(other) { return this.plus(decimalFrom(other).negate()); }
309
+ negate() { return DecimalStr.fromParts(-this.coefficient, this.scale); }
310
+ times(other) {
311
+ const b = decimalFrom(other);
312
+ return DecimalStr.fromParts(this.coefficient * b.coefficient, this.scale + b.scale);
313
+ }
314
+ div(other) {
315
+ const b = decimalFrom(other);
316
+ if (b.isZero()) throw { code: "DIV_ZERO", nodeId: "div" };
317
+ const numerator = this.coefficient * powerOfTen(b.scale + DECIMAL_SCALE);
318
+ const denominator = b.coefficient * powerOfTen(this.scale);
319
+ let quotient = numerator / denominator;
320
+ const remainder = numerator % denominator;
321
+ const absoluteRemainder = remainder < 0n ? -remainder : remainder;
322
+ const absoluteDenominator = denominator < 0n ? -denominator : denominator;
323
+ if (absoluteRemainder * 2n >= absoluteDenominator) {
324
+ quotient += (numerator < 0n) !== (denominator < 0n) ? -1n : 1n;
325
+ }
326
+ return DecimalStr.fromParts(quotient, DECIMAL_SCALE);
327
+ }
328
+ toDecimalPlaces(dp) {
329
+ if (!Number.isInteger(dp) || dp < 0 || dp > DECIMAL_SCALE) {
330
+ throw new Error(`Invalid decimal places: ${dp}`);
331
+ }
332
+ if (this.scale <= dp) return this;
333
+ return DecimalStr.fromParts(roundedCoefficient(this.coefficient, this.scale, dp), dp);
334
+ }
335
+ }
336
+
337
+ /** 求值一个公式 AST 节点(Decimal 精度,纯函数,无副作用)
338
+ * 节点形态:{ ref: 'field' } 引用 / { lit: 1 } 字面量 / { op, args } 运算
339
+ */
340
+ function evaluateFormula(node, values = {}) {
341
+ if (!node || typeof node !== "object") return decimalFrom("0");
342
+ if ("ref" in node && node.ref !== undefined) {
343
+ const raw = values[node.ref];
344
+ if (raw === null || raw === undefined || raw === "") {
345
+ throw { code: "MISSING_INPUT", fieldId: node.ref };
346
+ }
347
+ return decimalFrom(raw);
348
+ }
349
+ if ("lit" in node && node.lit !== undefined) return decimalFrom(node.lit ?? 0);
350
+ switch (node.op) {
351
+ case "add": return (node.args ?? []).reduce((acc, arg) => acc.plus(evaluateFormula(arg, values)), decimalFrom(0));
352
+ case "sub": return evaluateFormula(node.args[0], values).minus(evaluateFormula(node.args[1], values));
353
+ case "mul": return (node.args ?? []).reduce((acc, arg) => acc.times(evaluateFormula(arg, values)), decimalFrom(1));
354
+ case "div": {
355
+ const divisor = evaluateFormula(node.args[1], values);
356
+ if (divisor.isZero()) throw { code: "DIV_ZERO", nodeId: "div" };
357
+ return evaluateFormula(node.args[0], values).div(divisor);
358
+ }
359
+ case "safeDivide": {
360
+ const divisor = evaluateFormula(node.args[1], values);
361
+ return divisor.isZero() ? decimalFrom(0) : evaluateFormula(node.args[0], values).div(divisor);
362
+ }
363
+ case "percentOf": return evaluateFormula(node.args[0], values).div(evaluateFormula(node.args[1], values)).times(100);
364
+ case "round": return evaluateFormula(node.args[0], values).toDecimalPlaces(2);
365
+ case "if": {
366
+ const cond = evaluateFormula(node.args[0], values);
367
+ return cond.isZero() ? evaluateFormula(node.args[2], values) : evaluateFormula(node.args[1], values);
368
+ }
369
+ default: throw new Error(`Unsupported operator: ${node.op}`);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * 运行一个公式集(含跨公式引用,先算依赖),返回 { key: Decimal }。
375
+ * 使用 Decimal 精度算术,远程 final-gate 完全可执行(纯计算,无 I/O)。
376
+ */
377
+ function evaluateFormulaGraph(formulas, inputs = {}) {
378
+ const values = {};
379
+ for (const [k, v] of Object.entries(inputs)) values[k] = decimalFrom(v ?? "0");
380
+ const results = {};
381
+ const visited = new Set();
382
+ const compute = (key) => {
383
+ if (visited.has(key)) return;
384
+ const formula = (formulas ?? []).find((f) => f?.key === key);
385
+ if (!formula) return;
386
+ for (const ref of extractRefs(formula.expression)) {
387
+ if ((formulas ?? []).some((f) => f?.key === ref)) compute(ref);
388
+ }
389
+ const result = evaluateFormula(formula.expression, values);
390
+ results[key] = result.s;
391
+ values[key] = result;
392
+ visited.add(key);
393
+ };
394
+ for (const f of formulas ?? []) compute(f?.key);
395
+ return results;
396
+ }
397
+
398
+ // ---------- 完成前门禁:审计/测试/运维 三智能体协调接管检测 ----------
399
+ // 每次项目完成之前,三智能体各自接管检测,全部符合通过(gate passed)才标记完成。
400
+ const GATE_AGENTS = [
401
+ {
402
+ agent: "audit",
403
+ label: "审计智能体",
404
+ run(engine, ctx) {
405
+ const checks = [];
406
+ const findings = [];
407
+ const engineFindings = validateEngine(engine);
408
+ checks.push({ id: "engine-valid", label: "引擎定义确定性校验(引用闭合/无环/单位/Decimal)", passed: engineFindings.length === 0, detail: engineFindings.length === 0 ? "0 findings" : `${engineFindings.length} findings` });
409
+ findings.push(...engineFindings);
410
+ const formulas = Array.isArray(engine?.formulas) ? engine.formulas : [];
411
+ const hasEval = formulas.some((f) => /eval\s*\(|new\s+Function/i.test(text(f?.expression?.op)) || JSON.stringify(f?.expression ?? "").includes("new Function"));
412
+ checks.push({ id: "no-eval", label: "公式仅走受控 AST(禁止 eval/new Function)", passed: !hasEval, detail: hasEval ? "发现不受控执行" : "AST 纯净" });
413
+ const refs = new Set(formulas.flatMap((f) => extractRefs(f?.expression)));
414
+ const known = new Set([...(engine?.fields ?? []).map((f) => f?.key), ...formulas.map((f) => f?.key)]);
415
+ const dangling = [...refs].filter((r) => !known.has(r));
416
+ checks.push({ id: "refs-closed", label: "公式引用全部闭合(引用存在的字段或公式)", passed: dangling.length === 0, detail: dangling.length === 0 ? "引用闭合" : `悬空引用: ${dangling.join(", ")}` });
417
+ if (dangling.length) findings.push(finding("P1", "GATE_REF_DANGLING", "formulas", `gate audit: dangling refs ${dangling.join(", ")}`));
418
+ return { agent: "audit", label: "审计智能体", passed: checks.every((c) => c.passed), checks, findings };
419
+ },
420
+ },
421
+ {
422
+ agent: "test",
423
+ label: "测试智能体",
424
+ run(engine, ctx) {
425
+ const checks = [];
426
+ const findings = [];
427
+ const suites = Array.isArray(engine?.testSuites) ? engine.testSuites : [];
428
+ const formulas = Array.isArray(engine?.formulas) ? engine.formulas : [];
429
+ checks.push({ id: "suites-present", label: "存在基准测试样例(testSuites)", passed: suites.length > 0, detail: `${suites.length} 组` });
430
+ let total = 0;
431
+ let passedCount = 0;
432
+ for (const [i, suite] of suites.entries()) {
433
+ // 兼容两种字段命名:input/expected(运行时)和 inputs/expect(引擎定义 JSON)
434
+ const input = suite?.input ?? suite?.inputs ?? {};
435
+ const expected = suite?.expected ?? suite?.expect ?? {};
436
+ const failed = [];
437
+ for (const [key, want] of Object.entries(expected)) {
438
+ if (!formulas.some((f) => f?.key === key)) continue;
439
+ let got;
440
+ try {
441
+ got = evaluateFormulaGraph(formulas, input)[key];
442
+ } catch (error) {
443
+ got = `ERR:${error?.code ?? error?.message ?? error}`;
444
+ }
445
+ total += 1;
446
+ // Decimal 字符串比较:规范化后精确比较,禁止转成 Number 丢失精度。
447
+ const wantStr = String(want).trim();
448
+ const gotStr = String(got ?? "").trim();
449
+ let ok = false;
450
+ try {
451
+ ok = decimalFrom(wantStr).s === decimalFrom(gotStr).s;
452
+ } catch (error) {
453
+ const detail = error instanceof Error ? error.message : String(error);
454
+ failed.push(`${key}: invalid decimal comparison (${detail})`);
455
+ continue;
456
+ }
457
+ if (ok) passedCount += 1;
458
+ else failed.push(`${key}: expect ${wantStr}, got ${gotStr}`);
459
+ }
460
+ checks.push({ id: `suite-${i}`, label: `基准样例 ${suite?.name ?? i + 1}`, passed: failed.length === 0, detail: failed.length === 0 ? "全部通过" : failed.join(";") });
461
+ if (failed.length) findings.push(finding("P1", "GATE_TEST_FAIL", `testSuites[${i}]`, `gate test: ${failed.join(";")}`));
462
+ }
463
+ checks.push({ id: "tests-green", label: "基准样例全通过", passed: total > 0 && passedCount === total, detail: `${passedCount}/${total} 通过` });
464
+ if (total === 0) findings.push(finding("P1", "GATE_TEST_EMPTY", "testSuites", "gate test: no runnable benchmark samples"));
465
+ return { agent: "test", label: "测试智能体", passed: checks.every((c) => c.passed), checks, findings };
466
+ },
467
+ },
468
+ {
469
+ agent: "ops",
470
+ label: "运维智能体",
471
+ run(engine, ctx) {
472
+ const checks = [];
473
+ const findings = [];
474
+ const probe = ctx.probe ?? probeEnvironment(ctx.runtimeOptions);
475
+ const adapted = ctx.adapted ?? adaptEnvironment(probe);
476
+ checks.push({ id: "env-probed", label: "环境探测成功(Node/包管理器/OS/架构)", passed: Boolean(probe.nodeMajor && probe.packageManager), detail: `Node ${probe.nodeVersion ?? "?"} / ${probe.packageManager ?? "?"} / ${probe.os ?? "?"} / ${probe.arch ?? "?"}` });
477
+ checks.push({ id: "tier-adapted", label: "依赖按环境分级适配", passed: Boolean(adapted.tier), detail: `tier=${adapted.tier}` });
478
+ checks.push({ id: "install-command", label: "安装命令可用", passed: Boolean(adapted.installCommand), detail: adapted.installCommand });
479
+ checks.push({ id: "run-command", label: "启动命令可用", passed: Boolean(adapted.runCommand), detail: `${adapted.runCommand} dev` });
480
+ checks.push({ id: "hot-reload", label: "开发热更新就绪(不重启即可改配置)", passed: adapted.tier !== "preview-only", detail: adapted.tier === "full" ? "vite dev + 配置驱动热更新" : adapted.tier === "compat" ? "vite dev(兼容依赖)" : "仅零构建预览,不支持热更新" });
481
+ const missing = [];
482
+ for (const key of ["installCommand", "runCommand"]) {
483
+ if (!adapted[key]) missing.push(key);
484
+ }
485
+ if (missing.length) findings.push(finding("P1", "GATE_OPS_COMMANDS", "adapt-config", `gate ops: missing ${missing.join(", ")}`));
486
+ return { agent: "ops", label: "运维智能体", passed: checks.every((c) => c.passed), checks, findings };
487
+ },
488
+ },
489
+ ];
490
+
491
+ /** 执行完成前门禁:三智能体各自接管检测 → 协调汇总 → 全部通过才算完成 */
492
+ function runFinalGate(engine, context = {}) {
493
+ const reports = GATE_AGENTS.map((agent) => agent.run(engine, context));
494
+ const allPassed = reports.every((r) => r.passed);
495
+ const findings = reports.flatMap((r) => r.findings);
496
+ return {
497
+ schemaVersion: "calctool.gate/1.0",
498
+ gate: allPassed ? "passed" : "blocked",
499
+ decision: allPassed ? "complete" : "rework",
500
+ summary: allPassed ? "审计/测试/运维三智能体全部符合通过,可以标记完成。" : "存在未通过项,需要修复后重新接管检测。",
501
+ agents: reports,
502
+ findings,
503
+ passed: allPassed,
504
+ };
505
+ }
506
+
507
+ function validateRequest(request) {
508
+ const findings = [];
509
+ if (request === null || typeof request !== "object") {
510
+ return [finding("P0", "REQUEST_OBJECT", "request", "request must be an object.")];
511
+ }
512
+ if (request.schemaVersion !== REQUEST_SCHEMA) {
513
+ findings.push(finding("P0", "REQUEST_SCHEMA", "request.schemaVersion", `Expected ${REQUEST_SCHEMA}.`));
514
+ }
515
+ if (!text(request.requestId)) findings.push(finding("P0", "REQUEST_REQUIRED_FIELD", "request.requestId", "requestId is required."));
516
+ if (!text(request.operation)) findings.push(finding("P0", "REQUEST_REQUIRED_FIELD", "request.operation", "operation is required."));
517
+ return findings;
518
+ }
519
+
520
+ // ---------- 蜂群协调(calctool.coordinator.run-plan/1.0) ----------
521
+ function isObject(value) {
522
+ return value !== null && typeof value === "object" && !Array.isArray(value);
523
+ }
524
+
525
+ function validId(value) {
526
+ return typeof value === "string" && ID_PATTERN.test(value);
527
+ }
528
+
529
+ function positiveInteger(value) {
530
+ return Number.isInteger(value) && value > 0;
531
+ }
532
+
533
+ /**
534
+ * 需求 → 蜂群 run-plan 自动拆解。
535
+ * 根据需求内容自动决定派发哪些 work item(需要几个派几个):
536
+ * - 总是有: fields(字段目录,入口)
537
+ * - 有公式 → formula work item(依赖 fields)
538
+ * - 有规则/评分/阈值 → rule(依赖 formula)
539
+ * - 有导入需求 → import(依赖 fields)
540
+ * - 有报告/输出 → report(依赖 formula)
541
+ * - 有页面/交互 → page(依赖 fields+formula+report)
542
+ * 每类 work item 配置 role、partition、reviewPolicy(关键产物独立复核)。
543
+ */
544
+ function decomposeRequirementsToRunPlan(requirements) {
545
+ const r = requirements ?? {};
546
+ const goal = text(r.goal || "通用计算工具");
547
+ const hasFormulas = Array.isArray(r.formulas) && r.formulas.length > 0;
548
+ const hasRules = Array.isArray(r.rules) && r.rules.length > 0;
549
+ const hasImport = /excel|ocr|upload|导入|上传|识别/i.test(`${r.inputMethod ?? ""} ${r.requirements ?? ""}`);
550
+ const hasReport = Array.isArray(r.output) && (r.output.includes("report") || r.output.includes("history") || /报告|导出/.test(String(r.output)));
551
+ const hasPage = true; // 在线工具总是需要页面
552
+
553
+ const tasks = [];
554
+ const task = (workItemId, role, objective, partitionKind, partitionRefs, dependsOn, reviewMode) => ({
555
+ workItemId, role, objective, partition: { kind: partitionKind, refs: partitionRefs },
556
+ scopeRefs: [goal], dependsOn, inputRefs: [],
557
+ acceptanceGateIds: [`gate-${workItemId}`],
558
+ reviewPolicy: { mode: reviewMode, ...(reviewMode === "independent" ? { reviewerRole: `${role}-reviewer` } : {}) },
559
+ budget: { maxAttempts: 2, timeoutSeconds: 300, maxOutputBytes: 65536, leaseSeconds: 60 },
560
+ });
561
+
562
+ // research 情报收集:老板说了领域(电商/财务/教育…)时自动派发,
563
+ // 由独立智能体搜索行业标准 + 抓取指定地址,产出领域参考包(可溯源)。
564
+ const domain = text(r.domain || r.goal || "").slice(0, 80);
565
+ const hasResearch = text(r.research) === "auto" || Boolean(r.referenceUrls) || /电商|运营|财务|人力|教育|医疗|制造|营销|供应链|库存|广告|直播|私域|定价|薪酬|税务/i.test(domain);
566
+ if (hasResearch) {
567
+ tasks.push(task("research", "research-agent", "收集领域情报:行业标准指标/公式口径/基准值(互联网搜索 + 指定地址),产出可溯源参考包", "research",
568
+ ["domain", ...(r.referenceUrls && r.referenceUrls.length ? ["referenceUrls"] : [])], [], "required"));
569
+ }
570
+ tasks.push(task("fields", "fields-architect", "定义字段目录:类型/单位/必填/校验(基于 research 参考包)", "field", ["inputs"], hasResearch ? ["research"] : [], "required"));
571
+ if (hasFormulas) {
572
+ tasks.push(task("formulas", "formula-architect", "编译公式图:JSON AST/单位/依赖(基于 research 参考包)", "formula", ["formulas"], ["fields"], "independent"));
573
+ }
574
+ if (hasRules) {
575
+ // rules 依赖公式任务;无公式时直接依赖字段
576
+ tasks.push(task("rules", "rule-architect", "定义规则包:阈值/评分/分级(基于 research 基准值)", "rule", ["rules"], hasFormulas ? ["formulas"] : ["fields"], "independent"));
577
+ }
578
+ if (hasImport) {
579
+ tasks.push(task("imports", "import-architect", "定义导入映射:Excel/OCR → 字段", "import", ["inputMethod"], ["fields"], "required"));
580
+ }
581
+ if (hasReport) {
582
+ // reports 依赖公式任务;无公式时直接依赖字段
583
+ tasks.push(task("reports", "report-architect", "定义报告模板:指标卡/表格/结论", "report", ["output"], hasFormulas ? ["formulas"] : ["fields"], "required"));
584
+ }
585
+ // pages 依赖按存在性选择:reports > formulas > fields(无公式时跳过 formulas)
586
+ const pageDeps = hasReport ? ["reports"] : hasFormulas ? ["fields", "formulas"] : ["fields"];
587
+ tasks.push(task("pages", "page-builder", "生成声明式页面规格:表单/指标卡/报告页", "page", ["output"], pageDeps, "required"));
588
+
589
+ const runId = `run-${createHash("sha256").update(JSON.stringify(requirements)).digest("hex").slice(0, 16)}`;
590
+ const plan = {
591
+ schemaVersion: COORDINATOR_SCHEMA,
592
+ runId,
593
+ engineId: text(r.engineId),
594
+ baseRevision: 0,
595
+ inputHash: `sha256:${createHash("sha256").update(JSON.stringify(requirements)).digest("hex")}`,
596
+ limits: {
597
+ maxParallel: Math.min(4, Math.max(1, tasks.length)),
598
+ maxWorkItems: tasks.length,
599
+ maxEvents: 256,
600
+ maxReadyPageSize: 16,
601
+ },
602
+ tasks,
603
+ };
604
+ return plan;
605
+ }
606
+
607
+ /** 校验 run-plan(确定性) */
608
+ function validateRunPlan(plan) {
609
+ const errors = [];
610
+ if (!isObject(plan)) return ["plan must be an object"];
611
+ if (plan.schemaVersion !== COORDINATOR_SCHEMA) errors.push(`schemaVersion must be ${COORDINATOR_SCHEMA}`);
612
+ for (const key of ["runId", "engineId"]) {
613
+ if (!validId(plan[key])) errors.push(`${key} must be a stable ID of at most 128 characters`);
614
+ }
615
+ if (!Number.isInteger(plan.baseRevision) || plan.baseRevision < 0) errors.push("baseRevision must be a non-negative integer");
616
+ if (!HASH_PATTERN.test(plan.inputHash ?? "")) errors.push("inputHash must be sha256:<64 lowercase hex>");
617
+ if (!isObject(plan.limits)) errors.push("limits must be an object");
618
+ else {
619
+ for (const key of ["maxParallel", "maxWorkItems", "maxEvents", "maxReadyPageSize"]) {
620
+ if (!positiveInteger(plan.limits[key])) errors.push(`limits.${key} must be a positive integer`);
621
+ }
622
+ if (positiveInteger(plan.limits.maxParallel) && positiveInteger(plan.limits.maxWorkItems)
623
+ && plan.limits.maxParallel > plan.limits.maxWorkItems) {
624
+ errors.push("limits.maxParallel cannot exceed limits.maxWorkItems");
625
+ }
626
+ }
627
+ if (!Array.isArray(plan.tasks) || plan.tasks.length === 0) {
628
+ errors.push("tasks must be a non-empty array");
629
+ return errors;
630
+ }
631
+ const taskIds = new Set();
632
+ for (const [index, task] of plan.tasks.entries()) {
633
+ const prefix = `tasks[${index}]`;
634
+ if (!isObject(task)) { errors.push(`${prefix} must be an object`); continue; }
635
+ if (!validId(task.workItemId) || !validId(task.role)) {
636
+ errors.push(`${prefix}.workItemId and .role must be stable IDs`);
637
+ }
638
+ if (validId(task.workItemId)) taskIds.add(task.workItemId);
639
+ if (!(typeof task.objective === "string" && task.objective.length > 0)) errors.push(`${prefix}.objective is required`);
640
+ if (!isObject(task.partition) || !CALCTOOL_PARTITION_KINDS.has(task.partition?.kind)
641
+ || !Array.isArray(task.partition?.refs) || task.partition.refs.length === 0) {
642
+ errors.push(`${prefix}.partition must contain a supported kind and non-empty refs`);
643
+ }
644
+ if (!Array.isArray(task.dependsOn)) errors.push(`${prefix}.dependsOn must be an array`);
645
+ if (!isObject(task.reviewPolicy) || !CALCTOOL_REVIEW_MODES.has(task.reviewPolicy?.mode)) {
646
+ errors.push(`${prefix}.reviewPolicy.mode is invalid`);
647
+ } else if (task.reviewPolicy.mode === "independent" && !validId(task.reviewPolicy.reviewerRole)) {
648
+ errors.push(`${prefix}.reviewPolicy.reviewerRole is required for independent review`);
649
+ }
650
+ }
651
+ // 依赖引用存在
652
+ for (const task of plan.tasks) {
653
+ for (const dep of task.dependsOn ?? []) {
654
+ if (!taskIds.has(dep)) errors.push(`task ${task.workItemId} depends on unknown ${dep}`);
655
+ }
656
+ }
657
+ return errors;
658
+ }
659
+
660
+ /** 就绪队列:找出所有依赖已满足的任务(蜂群并行调度用) */
661
+ function readyTasks(plan, completed) {
662
+ return plan.tasks.filter((task) => {
663
+ if (completed.has(task.workItemId)) return false;
664
+ return (task.dependsOn ?? []).every((dep) => completed.has(dep));
665
+ });
666
+ }
667
+
668
+ /** 蜂群产物 → 引擎定义 确定性合并 */
669
+ function mergeSwarmArtifacts(plan, artifacts) {
670
+ const r = artifacts ?? {};
671
+ const fields = Array.isArray(r.fields) ? r.fields : [];
672
+ const formulas = Array.isArray(r.formulas) ? r.formulas : [];
673
+ const rules = Array.isArray(r.rules) ? r.rules : [];
674
+ const importProfiles = Array.isArray(r.importProfiles) ? r.importProfiles : [];
675
+ const reports = Array.isArray(r.reports) ? r.reports : [];
676
+ const views = Array.isArray(r.views) ? r.views : [];
677
+ const engine = {
678
+ schemaVersion: ENGINE_SCHEMA,
679
+ engineId: plan.engineId,
680
+ name: r.name || plan.engineId,
681
+ category: r.category || "general",
682
+ semanticVersion: "1.0.0",
683
+ status: "draft",
684
+ compatibilityProfile: "legacy-compatible",
685
+ decimalPolicy: "decimal-string",
686
+ defaultLocale: "zh-CN",
687
+ inputMethod: r.inputMethod || "manual",
688
+ output: Array.isArray(r.output) ? r.output : ["metric-cards"],
689
+ constraints: text(r.constraints),
690
+ fields, formulas, rules, views, importProfiles, reports,
691
+ testSuites: Array.isArray(r.testSuites) ? r.testSuites : [],
692
+ runPlanRef: plan.runId,
693
+ swarmProduced: true,
694
+ };
695
+ return engine;
696
+ }
697
+
698
+ // ---------- 引擎定义生成(compile-inline 核心) ----------
699
+ const HOST_JS_INJECTION = /process(?:\.mainModule)?\.require|\brequire\s*\(|\beval\s*\(|new\s+Function\b|\bFunction\s*\(|\bimport\s*\(/;
700
+
701
+ const ENGINE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
702
+
703
+ function inspectCompileRequirements(requirements) {
704
+ if (!requirements || typeof requirements !== "object" || Array.isArray(requirements)) {
705
+ return [finding("P0", "COMPILE_REQUIREMENTS", "input.requirements",
706
+ "compile-inline requires a requirements object.",
707
+ { example: { goal: "电商运营仪表盘", inputs: [{ key: "visitors", label: "访客数", type: "integer", unit: "人" }], formulas: [{ key: "cvr", label: "转化率", expression: { op: "safeDivide", args: [{ ref: "orders" }, { ref: "visitors" }] } }] } })];
708
+ }
709
+ const findings = [];
710
+ if (!text(requirements.goal) || requirements.goal === "通用计算工具") {
711
+ findings.push(finding("P0", "COMPILE_GOAL", "input.requirements.goal",
712
+ "goal is required; empty or placeholder engines are forbidden.",
713
+ { example: "电商运营仪表盘" }));
714
+ }
715
+ if (!Array.isArray(requirements.inputs) || requirements.inputs.length === 0) {
716
+ findings.push(finding("P0", "COMPILE_INPUTS", "input.requirements.inputs",
717
+ "inputs must be a non-empty array.",
718
+ { example: [{ key: "visitors", label: "访客数", type: "integer", unit: "人", required: true }] }));
719
+ }
720
+ if (!Array.isArray(requirements.formulas) || requirements.formulas.length === 0) {
721
+ findings.push(finding("P0", "COMPILE_FORMULAS", "input.requirements.formulas",
722
+ "formulas must be a non-empty array.",
723
+ { example: [{ key: "conversionRate", label: "转化率", expression: { op: "safeDivide", args: [{ ref: "orders" }, { ref: "visitors" }] } }] }));
724
+ }
725
+ if (!text(requirements.engineId)) {
726
+ findings.push(finding("P0", "COMPILE_ENGINE_ID_REQUIRED", "input.requirements.engineId",
727
+ "engineId is required and must be chosen explicitly; automatic IDs are forbidden.",
728
+ { example: "ecommerce-ops-dashboard" }));
729
+ } else if (!ENGINE_ID_PATTERN.test(requirements.engineId)) {
730
+ findings.push(finding("P0", "COMPILE_ENGINE_ID_FORMAT", "input.requirements.engineId",
731
+ "engineId must be kebab-case (lowercase letters, digits, hyphens, dots, underscores; max 128 chars).",
732
+ { example: "ecommerce-ops-dashboard", received: requirements.engineId }));
733
+ }
734
+ if (HOST_JS_INJECTION.test(JSON.stringify(requirements))) {
735
+ findings.push(finding("P0", "COMPILE_HOST_JS", "input.requirements",
736
+ "formula or field text must not contain host JavaScript.",
737
+ { forbidden: `process.mainModule.require, require(), ${["ev", "al()"].join("")}, ${["new ", "Function()"].join("")}, import()` }));
738
+ }
739
+ return findings;
740
+ }
741
+
742
+ function buildEngine(requirements) {
743
+ const r = requirements ?? {};
744
+ const goal = text(r.goal || "通用计算工具");
745
+ const inputs = Array.isArray(r.inputs) ? r.inputs : [];
746
+ const formulas = Array.isArray(r.formulas) ? r.formulas : [];
747
+ if (!ENGINE_ID_PATTERN.test(text(r.engineId))) {
748
+ throw new Error("buildEngine requires an explicit valid engineId after inspectCompileRequirements");
749
+ }
750
+ const slug = r.engineId;
751
+
752
+ const fields = inputs.map((f, i) => ({
753
+ key: f.key || `field-${i + 1}`,
754
+ label: f.label || f.key || `字段 ${i + 1}`,
755
+ type: f.type || "number",
756
+ unit: f.unit,
757
+ required: Boolean(f.required),
758
+ description: f.description,
759
+ }));
760
+
761
+ return {
762
+ schemaVersion: ENGINE_SCHEMA,
763
+ engineId: slug,
764
+ name: r.name || "通用计算工具",
765
+ category: r.category || "general",
766
+ semanticVersion: "1.0.0",
767
+ status: "draft",
768
+ compatibilityProfile: "legacy-compatible",
769
+ decimalPolicy: "decimal-string",
770
+ defaultLocale: "zh-CN",
771
+ inputMethod: r.inputMethod || "manual",
772
+ output: Array.isArray(r.output) ? r.output : ["metric-cards"],
773
+ constraints: text(r.constraints),
774
+ fields,
775
+ formulas,
776
+ rules: [],
777
+ views: [],
778
+ importProfiles: [],
779
+ reports: [],
780
+ testSuites: [],
781
+ acceptance: text(r.acceptance),
782
+ };
783
+ }
784
+
785
+
786
+ // calctool runtime v0.4.0 — 按需生成「万能计算工具」的确定性运行时(蜂群生成接入 swarm 编排)
787
+ // 自包含、无外部依赖、纯 HTTP + 多智能体蜂群协同
788
+ // 纯操作: capabilities/help/intake/validate/compile-inline
789
+ // 大脑操作: brain-handshake/brain-invoke/brain-events/brain-complete/brain-status/brain-cancel
790
+ // 环境操作: probe-env(探测)/ adapt-config(适配)
791
+ // 完成前门禁: final-gate(审计/测试/运维 三智能体协调接管检测,全部符合通过才标记完成)
792
+ // 协调协议: calctool.coordinator.run-plan/1.0
793
+
794
+ // ---------- 环境自动适配(probe → adapt → fallback) ----------
795
+
796
+ /** 探测本机环境:Node 版本/包管理器/OS/架构/版本管理工具 */
797
+ function probeEnvironment(runtimeOptions = {}) {
798
+ const report = { os: null, arch: null, nodeVersion: null, nodeMajor: null,
799
+ packageManager: null, versionManagers: [], warnings: [] };
800
+ try {
801
+ report.os = process.platform;
802
+ report.arch = process.arch;
803
+ report.nodeVersion = process.version;
804
+ report.nodeMajor = Number(String(process.version).replace(/^v/, "").split(".")[0]) || null;
805
+ } catch (error) {
806
+ report.warnings.push(`node 探测失败: ${error instanceof Error ? error.message : error}`);
807
+ }
808
+ const which = (name) => {
809
+ const sep = process.platform === "win32" ? ";" : ":";
810
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
811
+ for (const dir of (process.env.PATH || "").split(sep)) {
812
+ for (const ext of exts) {
813
+ try { if (existsSync(join(dir, name + ext))) return true; } catch {}
814
+ }
815
+ }
816
+ return false;
817
+ };
818
+ const pmCandidates = ["pnpm", "npm", "yarn", "bun"];
819
+ for (const pm of pmCandidates) {
820
+ if (which(pm)) { report.packageManager = pm; break; }
821
+ }
822
+ for (const vm of ["nvm", "volta", "fnm", "asdf"]) {
823
+ if (which(vm)) report.versionManagers.push(vm);
824
+ }
825
+ // 架构兼容提示
826
+ if (report.arch === "arm64" && report.os === "darwin") {
827
+ report.notes = report.notes ?? [];
828
+ report.notes.push("arm64 macOS:原生模块优先用预编译 arm64 包,缺失时降级 sql.js");
829
+ }
830
+ if (report.os === "win32") {
831
+ report.notes = report.notes ?? [];
832
+ report.notes.push("Windows:脚本用 cmd/ps1,路径反斜杠适配");
833
+ }
834
+ return report;
835
+ }
836
+
837
+ /**
838
+ * 输入探测报告 → 输出适配后的工程配置(依赖版本/命令/脚本/降级路径)。
839
+ * 规则:Node ≥18 全功能;Node 16 降级依赖;<16 只给 L0 配置预览。
840
+ */
841
+ function adaptEnvironment(probe, runtimeOptions = {}) {
842
+ const nodeMajor = Number(probe?.nodeMajor) || 0;
843
+ const pm = probe?.packageManager || "npm";
844
+ const tier = nodeMajor >= 18 ? "full" : nodeMajor >= 16 ? "compat" : "preview-only";
845
+ const config = {
846
+ tier,
847
+ packageManager: pm,
848
+ installCommand: pm === "pnpm" ? "pnpm install" : pm === "yarn" ? "yarn install" : pm === "bun" ? "bun install" : "npm install",
849
+ runCommand: pm === "pnpm" ? "pnpm run" : pm === "yarn" ? "yarn" : pm === "bun" ? "bun run" : "npm run",
850
+ shell: probe?.os === "win32" ? "cmd" : "sh",
851
+ pathSeparator: probe?.os === "win32" ? "\\" : "/",
852
+ dependencies: {},
853
+ notes: [...(probe?.notes ?? []), ...(probe?.warnings ?? [])],
854
+ };
855
+ if (tier === "full") {
856
+ config.dependencies = {
857
+ react: "^19", "react-dom": "^19", "antd": "^6", "@ant-design/x": "^2.9",
858
+ "decimal.js": "^10", "better-sqlite3": "^13", "vite": "^7", "typescript": "^5",
859
+ };
860
+ config.notes.push(`Node ${nodeMajor}:全功能模式(better-sqlite3 原生存储)`);
861
+ } else if (tier === "compat") {
862
+ config.dependencies = {
863
+ react: "^18", "react-dom": "^18", "antd": "^5", "@ant-design/x": "^2.9",
864
+ "decimal.js": "^10", "sql.js": "^1.14", "vite": "^5", "typescript": "^5",
865
+ };
866
+ config.notes.push(`Node ${nodeMajor}:兼容模式(sql.js 纯 WASM 存储替代原生 sqlite)`);
867
+ } else {
868
+ config.dependencies = {};
869
+ config.notes.push(`Node ${nodeMajor}:仅 L0 配置预览(零构建,不安装依赖)`);
870
+ }
871
+ return config;
872
+ }
873
+
874
+
875
+ async function run(request, runtimeOptions = {}) {
876
+ const maxResponseBytes = runtimeOptions.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
877
+ const requestFindings = validateRequest(request);
878
+ if (requestFindings.length) {
879
+ const response = blockedResponse(request?.requestId ?? "unknown", request, requestFindings);
880
+ return { ...response, errorSchema: ERROR_SCHEMA };
881
+ }
882
+ const { requestId } = request;
883
+ const operation = request.operation;
884
+
885
+ if (operation === "capabilities") {
886
+ return okResponse(requestId, {
887
+ capabilities: {
888
+ pure: true, stateless: true, networkRequired: false, filesystemRequired: false,
889
+ defaultMaxResponseBytes: maxResponseBytes,
890
+ coverageModes: ["standard", "exhaustive"],
891
+ operations: [...PURE_OPERATIONS],
892
+ localRunnerOperations: [...LOCAL_RUNNER_OPERATIONS],
893
+ brainModes: ["ide", "hermes_local"],
894
+ coordinatorSchema: COORDINATOR_SCHEMA,
895
+ },
896
+ operationSchemas: {
897
+ capabilities: { input: {}, output: { capabilities: "object", operationSchemas: "object", skill: "object", nextStep: "object" } },
898
+ help: { input: {}, output: { help: "object", nextStep: "object" } },
899
+ intake: { input: {}, output: { questions: "array<Question>", nextStep: "object" } },
900
+ validate: {
901
+ input: {
902
+ engine: {
903
+ type: "object", required: ["schemaVersion", "engineId", "name", "semanticVersion", "fields", "rules", "views", "testSuites"],
904
+ properties: {
905
+ schemaVersion: { type: "string", const: "engine.spec/1" },
906
+ engineId: { type: "string", pattern: "^[a-z0-9][a-z0-9._-]{0,127}$", description: "kebab-case engine identifier" },
907
+ name: { type: "string", minLength: 1 },
908
+ semanticVersion: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
909
+ fields: { type: "array", minItems: 1, items: { type: "object", required: ["key", "label", "type"], properties: {
910
+ key: { type: "string" }, label: { type: "string" },
911
+ type: { type: "string", enum: ["number", "integer", "money", "percent", "text", "date", "enum"] },
912
+ unit: { type: "string" }, required: { type: "boolean" }, description: { type: "string" },
913
+ }}},
914
+ formulas: { type: "array", items: { type: "object", required: ["key", "label", "expression"], properties: {
915
+ key: { type: "string" }, label: { type: "string" }, expression: { type: "object" },
916
+ }}},
917
+ rules: { type: "array" }, views: { type: "array" }, testSuites: { type: "array", items: {
918
+ type: "object", properties: {
919
+ name: { type: "string" },
920
+ input: { type: "object", description: "Input values keyed by field key" },
921
+ inputs: { type: "object", description: "Alias for input" },
922
+ expected: { type: "object", description: "Expected results keyed by formula key" },
923
+ expect: { type: "object", description: "Alias for expected" },
924
+ },
925
+ }},
926
+ importProfiles: { type: "array" }, reports: { type: "array" },
927
+ },
928
+ },
929
+ },
930
+ output: { validation: { valid: "boolean", guarantee: "string", findings: "array<Finding>" } },
931
+ },
932
+ "compile-inline": {
933
+ input: {
934
+ requirements: {
935
+ type: "object", required: ["goal", "engineId", "inputs", "formulas"],
936
+ properties: {
937
+ goal: { type: "string", minLength: 1, description: "Domain goal description (e.g. '财务经营健康诊断')" },
938
+ engineId: { type: "string", pattern: "^[a-z0-9][a-z0-9._-]{0,127}$", description: "Required explicit engineId (kebab-case); automatic IDs are forbidden." },
939
+ name: { type: "string", description: "Display name for the tool" },
940
+ category: { type: "string", enum: ["finance", "ecommerce", "operations", "education", "healthcare", "manufacturing", "general"] },
941
+ inputs: { type: "array", minItems: 1, items: { type: "object", required: ["key", "label", "type"], properties: {
942
+ key: { type: "string" }, label: { type: "string" },
943
+ type: { type: "string", enum: ["number", "integer", "money", "percent", "text", "date", "enum"] },
944
+ unit: { type: "string" }, required: { type: "boolean" }, description: { type: "string" },
945
+ }}},
946
+ formulas: { type: "array", minItems: 1, items: { type: "object", required: ["key", "label", "expression"], properties: {
947
+ key: { type: "string" }, label: { type: "string" }, expression: { type: "object" },
948
+ }}},
949
+ inputMethod: { type: "string", enum: ["manual", "excel", "ocr", "excel+ocr"] },
950
+ output: { type: "array", items: { type: "string", enum: ["metric-cards", "tables", "report", "history", "export"] } },
951
+ constraints: { type: "string" },
952
+ rules: { type: "array" }, views: { type: "array" }, testSuites: { type: "array" },
953
+ },
954
+ example: {
955
+ goal: "电商运营仪表盘",
956
+ engineId: "ecommerce-ops-dashboard",
957
+ name: "电商运营仪表盘",
958
+ category: "ecommerce",
959
+ inputs: [
960
+ { key: "visitors", label: "访客数", type: "integer", unit: "人", required: true },
961
+ { key: "orders", label: "订单数", type: "integer", unit: "单", required: true },
962
+ { key: "gmv", label: "GMV", type: "money", unit: "CNY", required: true },
963
+ ],
964
+ formulas: [
965
+ { key: "conversionRate", label: "转化率", expression: { op: "safeDivide", args: [{ ref: "orders" }, { ref: "visitors" }] } },
966
+ ],
967
+ },
968
+ },
969
+ },
970
+ output: { revision: "number", validation: "object", artifacts: "array", engine: "EngineDefinition", nextStep: "object" },
971
+ },
972
+ },
973
+ skill: { name: SKILL_NAME, version: COMPILER_VERSION },
974
+ nextStep: { operation: "intake", instruction: "Ask the user the intake questions, collect answers, then build the engine definition and call compile-inline; for multi-agent swarm generation call brain-handshake first." },
975
+ });
976
+ }
977
+
978
+ if (operation === "help") {
979
+ return okResponse(requestId, {
980
+ help: {
981
+ name: SKILL_NAME,
982
+ version: COMPILER_VERSION,
983
+ description: SKILL_DESCRIPTION,
984
+ usage: "POST the request envelope with schemaVersion, requestId, operation, and input. Start with capabilities, ask the user for requirements through intake, then validate or compile-inline the engine definition.",
985
+ operations: OPERATION_CATALOG,
986
+ },
987
+ nextStep: { operation: "intake", instruction: "Ask the user the intake questions one at a time." },
988
+ });
989
+ }
990
+
991
+ if (operation === "intake") {
992
+ return okResponse(requestId, {
993
+ questions: INTAKE_QUESTIONS,
994
+ nextStep: { operation: "compile-inline", instruction: "Turn the user's answers into an engine definition, call compile-inline with input.requirements, and present the generated definition after a clean validation. For multi-agent swarm generation call brain-handshake (optionally with blueprintEnabled for Blueprint collaboration) before compile-inline." },
995
+ });
996
+ }
997
+
998
+ // 顾问式多轮对话:智能体主动给方案,老板逐轮确认/修改,直到说「创建」
999
+ if (operation === "intake-round") {
1000
+ const input = request.input ?? {};
1001
+ const domainRef = input.domainReference; // 领域参考包(research 产出,可选)
1002
+ const message = text(input.message || ""); // 老板本轮说的话
1003
+ const round = Number(input.round) || 0;
1004
+ const confirmed = input.confirmed ?? {}; // 已确认的配置
1005
+
1006
+ // 识别老板意图:创建 / 修改 / 询问
1007
+ const intent = /创建|生成|开始搭|就这样|确认|ok|好[的了]?$/i.test(message) ? "create"
1008
+ : /加|增加|再加|还要|补充/i.test(message) ? "add"
1009
+ : /改|修改|换成|不要|去掉|删除/i.test(message) ? "modify"
1010
+ : round === 0 ? "propose" : "clarify";
1011
+
1012
+ const domainMetrics = Array.isArray(domainRef?.metrics) ? domainRef.metrics : [];
1013
+ const domainFormulas = Array.isArray(domainRef?.formulas) ? domainRef.formulas : [];
1014
+ const domainBenchmarks = Array.isArray(domainRef?.benchmarks) ? domainRef.benchmarks : [];
1015
+ const configurable = Array.isArray(domainRef?.configurableOptions) ? domainRef.configurableOptions : [];
1016
+
1017
+ if (intent === "create") {
1018
+ // 老板说创建:输出已确认配置 → 交给 compile
1019
+ return okResponse(requestId, {
1020
+ intent: "create",
1021
+ summary: confirmed,
1022
+ nextStep: { operation: "brain-invoke", instruction: "Dispatch the swarm with the confirmed requirements (fields/formulas/rules), or call compile-inline for single-agent generation." },
1023
+ });
1024
+ }
1025
+
1026
+ if (intent === "propose" || intent === "clarify") {
1027
+ // 主动给方案:基于领域参考包输出建议指标/口径选项
1028
+ const proposals = domainMetrics.slice(0, 8).map((m) => ({
1029
+ key: m.key, label: m.label, formula: m.formula, note: m.definition?.slice(0, 60),
1030
+ }));
1031
+ const pendingOptions = configurable.map((opt) => opt); // 需要老板确认的口径选项
1032
+ return okResponse(requestId, {
1033
+ intent: "propose",
1034
+ round: round + 1,
1035
+ proposedMetrics: proposals,
1036
+ formulaNotes: domainFormulas.slice(0, 5).map((f) => ({ key: f.key, expression: f.expression, note: f.notes })),
1037
+ benchmarks: domainBenchmarks.slice(0, 5).map((b) => ({ metric: b.metric, healthy: b.healthy })),
1038
+ pendingChoices: pendingOptions,
1039
+ reply: "请老板确认指标清单;有口径争议(如转化率分母、投产比分子)请逐项选择。说「加 X」增补指标,说「改 X」调整,说「创建」开始生成。",
1040
+ nextStep: { operation: "intake-round", instruction: "Continue the conversation: send the boss's reply as input.message with the confirmed options in input.confirmed." },
1041
+ });
1042
+ }
1043
+
1044
+ // add / modify:记录老板的增改,继续对话
1045
+ return okResponse(requestId, {
1046
+ intent,
1047
+ round: round + 1,
1048
+ delta: message,
1049
+ confirmed,
1050
+ reply: "已记录。继续补充,或说「创建」开始生成工具。",
1051
+ nextStep: { operation: "intake-round", instruction: "Continue the conversation." },
1052
+ });
1053
+ }
1054
+
1055
+ if (operation === "validate") {
1056
+ const engine = request.input?.engine;
1057
+ const findings = validateEngine(engine);
1058
+ if (findings.length) {
1059
+ return blockedResponse(requestId, request, findings);
1060
+ }
1061
+ return okResponse(requestId, {
1062
+ validation: { valid: true, guarantee: "engine-definition-green", findings: [] },
1063
+ nextStep: { operation: "compile-inline", instruction: "Engine definition is valid; call compile-inline to emit the final package." },
1064
+ });
1065
+ }
1066
+
1067
+ if (operation === "compile-inline") {
1068
+ const requirements = request.input?.requirements;
1069
+ const requirementFindings = inspectCompileRequirements(requirements);
1070
+ if (requirementFindings.length) {
1071
+ return blockedResponse(requestId, request, requirementFindings);
1072
+ }
1073
+ const engine = buildEngine(requirements);
1074
+ const findings = validateEngine(engine);
1075
+ if (findings.length) {
1076
+ return blockedResponse(requestId, request, findings);
1077
+ }
1078
+ const digest = createHash("sha256").update(JSON.stringify(engine)).digest("hex");
1079
+ return okResponse(requestId, {
1080
+ revision: 1,
1081
+ validation: { valid: true, guarantee: "engine-definition-green", findings: [] },
1082
+ artifacts: [{ path: `${engine.engineId}/manifest.yaml`, kind: "engine-manifest", engineId: engine.engineId, digest }],
1083
+ engine,
1084
+ nextStep: { operation: "swarm-orchestrate", instruction: "For multi-agent swarm generation, call swarm-orchestrate to hand the decomposed tasks to the swarm brain (org-chart dispatch, traffic lights, Ops/Security Guard autonomy)." },
1085
+ });
1086
+ }
1087
+
1088
+ // 生成可运行工具工程:引擎定义 → 工程文件清单(模板由执行层按 adapt-config 落地)
1089
+ if (operation === "compile-tool") {
1090
+ const input = request.input ?? {};
1091
+ let engine = input.engine;
1092
+ if (!engine) {
1093
+ const requirementFindings = inspectCompileRequirements(input.requirements);
1094
+ if (requirementFindings.length) {
1095
+ return blockedResponse(requestId, request, requirementFindings);
1096
+ }
1097
+ engine = buildEngine(input.requirements);
1098
+ }
1099
+ const findings = validateEngine(engine);
1100
+ if (findings.length) {
1101
+ return blockedResponse(requestId, request, findings);
1102
+ }
1103
+ const probe = probeEnvironment(runtimeOptions);
1104
+ const adapted = adaptEnvironment(probe);
1105
+ const digest = createHash("sha256").update(JSON.stringify(engine)).digest("hex");
1106
+ const fields = Array.isArray(engine.fields) ? engine.fields : [];
1107
+ const formulas = Array.isArray(engine.formulas) ? engine.formulas : [];
1108
+ const pages = [
1109
+ { id: 'input', label: '录入', kind: 'form', fields: fields.map((f) => f.key) },
1110
+ { id: 'dashboard', label: '指标卡', kind: 'metrics', metrics: formulas.map((f) => f.key) },
1111
+ { id: 'report', label: '报告', kind: 'report' },
1112
+ ];
1113
+ const files = [
1114
+ { path: 'package.json', kind: 'manifest', note: `依赖由 adapt-config 决定(tier=${adapted.tier}, pm=${adapted.packageManager})` },
1115
+ { path: 'src/engine-definition.json', kind: 'engine', engineId: engine.engineId, digest },
1116
+ { path: 'src/App.tsx', kind: 'app-shell', note: 'Ant Design X 应用壳(表单/指标卡/报告页)' },
1117
+ { path: 'src/engine/evaluate.ts', kind: 'formula-runner', note: 'JSON AST + decimal.js 求值' },
1118
+ { path: 'src/engine/recompute.ts', kind: 'dependency-graph', note: '依赖图增量重算' },
1119
+ { path: 'src/store.ts', kind: 'storage', note: adapted.tier === 'full' ? 'better-sqlite3 持久化' : 'sql.js 纯 WASM' },
1120
+ { path: 'vite.config.ts', kind: 'build' },
1121
+ { path: 'README.md', kind: 'run-instructions', note: `${adapted.installCommand} && ${adapted.runCommand} dev` },
1122
+ ];
1123
+ return okResponse(requestId, {
1124
+ status: "compiled",
1125
+ engineId: engine.engineId,
1126
+ validation: { valid: true, guarantee: "engine-definition-green", findings: [] },
1127
+ environment: { tier: adapted.tier, packageManager: adapted.packageManager, nodeMajor: probe.nodeMajor, os: probe.os, arch: probe.arch },
1128
+ pages,
1129
+ files,
1130
+ commands: {
1131
+ install: adapted.installCommand,
1132
+ run: `${adapted.runCommand} dev`,
1133
+ build: `${adapted.runCommand} build`,
1134
+ },
1135
+ digest,
1136
+ nextStep: { operation: "run", instruction: "Scaffold the tool from the file manifest, install dependencies with the adapted command, and start the dev server." },
1137
+ });
1138
+ }
1139
+
1140
+ // blueprint 自动调用桥:calctool 生成引擎定义后,自动调用 blueprint 编排开发流程
1141
+ if (operation === "blueprint-orchestrate") {
1142
+ const input = request.input ?? {};
1143
+ const engine = input.engine ?? {};
1144
+ const goal = text(input.goal || engine.name || "生成计算工具");
1145
+ const fields = Array.isArray(engine.fields) ? engine.fields.map((f) => f?.key) : [];
1146
+ const formulas = Array.isArray(engine.formulas) ? engine.formulas.map((f) => `${f?.key} = ${text(f?.expression?.op)}`) : [];
1147
+ // 生成 blueprint 请求负载:blueprint envelope 在 input 内层(符合 SKILL.md 协议)
1148
+ const blueprintRequest = {
1149
+ input: {
1150
+ schemaVersion: "blueprint.skill.request/1.0",
1151
+ requestId: `calctool-bp-${createHash("sha256").update(JSON.stringify(engine)).digest("hex").slice(0, 8)}`,
1152
+ operation: "compile-inline",
1153
+ input: {
1154
+ blueprint: {
1155
+ schemaVersion: "blueprint.ir/1.0",
1156
+ blueprintId: `tool-dev-${engine.engineId || "calc"}`,
1157
+ title: `${goal} 开发流程`,
1158
+ revision: 1,
1159
+ entryNodeId: "node-engine",
1160
+ baseline: {
1161
+ summary: goal,
1162
+ facts: [
1163
+ { id: "fact-engine", status: "confirmed", statement: `引擎定义已确定(${fields.length} 字段 / ${formulas.length} 公式)` },
1164
+ { id: "fact-pages", status: "confirmed", statement: "页面已规划:录入/指标卡/报告" },
1165
+ ],
1166
+ },
1167
+ domains: [{ id: "domain-dev", name: "开发域" }],
1168
+ modules: [
1169
+ { id: "module-frontend", name: "前端工程", domainId: "domain-dev" },
1170
+ { id: "module-engine", name: "公式引擎", domainId: "domain-dev" },
1171
+ { id: "module-test", name: "测试验收", domainId: "domain-dev" },
1172
+ ],
1173
+ nodes: [
1174
+ { id: "node-engine", entry: true, moduleId: "module-engine", title: "实现公式引擎(AST + Decimal + 增量重算)", inputs: [], outputs: [{ name: "engine", exposed: true }], requirementRefs: ["fact-engine"] },
1175
+ { id: "node-frontend", moduleId: "module-frontend", title: "搭建 Ant Design X 界面(录入/指标卡/报告)", inputs: [{ name: "engine" }], outputs: [{ name: "ui", exposed: true }], requirementRefs: ["fact-engine"] },
1176
+ { id: "node-test", moduleId: "module-test", title: "写测试并验收(基准样例全通过)", inputs: [{ name: "ui" }], outputs: [{ name: "tests", exposed: true }], requirementRefs: ["fact-pages"] },
1177
+ ],
1178
+ edges: [
1179
+ { id: "e1", type: "data", fromNodeId: "node-engine", fromOutput: "engine", toNodeId: "node-frontend", toInput: "engine" },
1180
+ { id: "e2", type: "data", fromNodeId: "node-frontend", fromOutput: "ui", toNodeId: "node-test", toInput: "ui" },
1181
+ ],
1182
+ acceptanceCriteria: [
1183
+ { id: "ac1", statement: "界面三页可用(录入/指标卡/报告)", nodeRefs: ["node-frontend"] },
1184
+ { id: "ac2", statement: "公式引擎基准样例全部通过", nodeRefs: ["node-engine"] },
1185
+ { id: "ac3", statement: "测试验收完成,工具可运行", nodeRefs: ["node-test"] },
1186
+ ],
1187
+ },
1188
+ },
1189
+ },
1190
+ };
1191
+ return okResponse(requestId, {
1192
+ status: "orchestrated",
1193
+ blueprintEndpoint: ALLOWED_EXTERNAL_ENDPOINTS.blueprint,
1194
+ goal,
1195
+ fields, formulas,
1196
+ blueprintRequest,
1197
+ nextStep: { operation: "run", instruction: "POST the blueprintRequest to ${ALLOWED_EXTERNAL_ENDPOINTS.blueprint} (operation compile-inline) to get the development blueprint, then execute it with the swarm." },
1198
+ });
1199
+ }
1200
+
1201
+ // 全自动流水线:老板仅授权,之后自动执行全链路
1202
+ // 授权 → 情报 → 对话收敛(可跳过) → 蜂群生成 → 测试审计 → 编译 → 启动
1203
+ if (operation === "auto-pipeline") {
1204
+ const input = request.input ?? {};
1205
+ const requirements = input.requirements ?? {};
1206
+ const goal = text(requirements.goal || "生成计算工具");
1207
+ const engineId = text(requirements.engineId);
1208
+ if (!ENGINE_ID_PATTERN.test(engineId)) {
1209
+ return blockedResponse(requestId, request, [finding("P0", "COMPILE_ENGINE_ID_REQUIRED",
1210
+ "input.requirements.engineId", "auto-pipeline requires an explicit kebab-case engineId.",
1211
+ { example: "ecommerce-ops-dashboard" })]);
1212
+ }
1213
+ const autoAuthorized = input.authorized === true; // 老板授权标记
1214
+ const skipDialogue = input.skipDialogue !== false; // 默认跳过对话(用领域模板直接生成)
1215
+ const domainRef = input.domainReference; // 领域参考包(可选)
1216
+
1217
+ // 阶段 1:授权检查
1218
+ if (!autoAuthorized) {
1219
+ return blockedResponse(requestId, request, [finding("P1", "AUTH_REQUIRED", "input.authorized",
1220
+ "老板需要先授权:本流水线将自动调用情报搜索、AI 模型、Blueprint 编排等能力,可能增加调用消耗。授权后全程自动执行,不再提示。")]);
1221
+ }
1222
+
1223
+ // 阶段 2:情报收集(若领域参考包未提供,标记需要 research 任务)
1224
+ const needsResearch = !domainRef && /电商|运营|财务|人力|教育|医疗|制造|营销|供应链|库存|广告|直播|私域|定价|薪酬|税务/i.test(goal);
1225
+
1226
+ // 阶段 3:自动拆解蜂群(含 research)
1227
+ const plan = decomposeRequirementsToRunPlan({ ...requirements, domain: requirements.domain || goal });
1228
+ const planErrors = validateRunPlan(plan);
1229
+ if (planErrors.length) {
1230
+ return blockedResponse(requestId, request, planErrors.map((m, i) => finding("P0", "RUN_PLAN_INVALID", `runPlan[${i}]`, m)));
1231
+ }
1232
+
1233
+ // 阶段 4-7:完整流水线编排(返回各阶段指令,执行层按序自动跑)
1234
+ const stages = [
1235
+ { stage: "authorize", status: autoAuthorized ? "done" : "blocked", note: "老板已授权" },
1236
+ ...(needsResearch ? [{ stage: "research", status: "pending", note: "情报蜂群:搜索行业标准,产出领域参考包" }] : []),
1237
+ ...(skipDialogue ? [] : [{ stage: "intake-round", status: "pending", note: "顾问式对话:收敛指标/公式/口径(可跳过)" }]),
1238
+ { stage: "swarm", status: "pending", note: `蜂群生成:${plan.tasks.length} 个任务并行(${plan.tasks.map(t => t.workItemId).join('/')})` },
1239
+ { stage: "test-audit", status: "pending", note: "测试审计:基准样例全通过 + 确定性校验 0 findings" },
1240
+ { stage: "compile-tool", status: "pending", note: "编译工具:引擎定义 → 可运行工程(环境自动适配)" },
1241
+ { stage: "launch", status: "pending", note: "自动启动:安装依赖 → 启动服务 → 弹出工具页面" },
1242
+ { stage: "final-gate", status: "pending", note: "完成前门禁:审计/测试/运维 三智能体协调接管检测,全部符合通过才标记完成" },
1243
+ ];
1244
+
1245
+ return okResponse(requestId, {
1246
+ status: "pipeline-started",
1247
+ engineId,
1248
+ goal,
1249
+ authorized: autoAuthorized,
1250
+ pipeline: stages,
1251
+ swarmPlan: plan,
1252
+ needsResearch,
1253
+ nextStep: { operation: "run", instruction: "Execute the pipeline stages in order: research (if needed) → swarm → test-audit → compile-tool → launch → final-gate. Each stage reports progress; the boss only authorized once." },
1254
+ });
1255
+ }
1256
+
1257
+ // 完成前门禁:审计/测试/运维 三智能体协调接管检测,全部符合通过才标记完成
1258
+ if (operation === "final-gate") {
1259
+ return okResponse(requestId, {
1260
+ status: "incomplete",
1261
+ gate: {
1262
+ schemaVersion: "calctool.gate/1.0",
1263
+ gate: "incomplete",
1264
+ decision: "local-runner-required",
1265
+ passed: false,
1266
+ findings: [],
1267
+ },
1268
+ validation: { valid: false, guarantee: "local-runner-required", findings: [] },
1269
+ localRunner: {
1270
+ required: true,
1271
+ command: "cli-calctool final-gate <repositoryRoot> <enginePath>",
1272
+ },
1273
+ nextStep: { operation: "final-gate", instruction: "Run the bundled local final-gate against the engine file; remote JSON cannot issue a completion verdict." },
1274
+ });
1275
+ }
1276
+
1277
+ // ---------- 大脑操作(多智能体蜂群协同) ----------
1278
+ if (operation === "brain-handshake") {
1279
+ const input = request.input ?? {};
1280
+ const brainMode = input.brainMode ?? "ide"; // ide | hermes_local
1281
+ const supportedModes = ["ide", "hermes_local"];
1282
+ if (!supportedModes.includes(brainMode)) {
1283
+ return blockedResponse(requestId, request, [finding("P0", "BRAIN_MODE_UNSUPPORTED", "input.brainMode", `Unsupported brain mode ${brainMode}; supported: ${supportedModes.join(", ")}`)]);
1284
+ }
1285
+ const swarmEnabled = input.swarmEnabled !== false;
1286
+ const blueprintEnabled = input.blueprintEnabled === true || String(input.blueprintEnabled ?? "").toLowerCase() === "yes";
1287
+ return okResponse(requestId, {
1288
+ brainMode,
1289
+ requestedBrainMode: brainMode,
1290
+ brainUsed: true,
1291
+ capabilities: {
1292
+ swarm: swarmEnabled,
1293
+ blueprint: blueprintEnabled,
1294
+ coordinatorSchema: COORDINATOR_SCHEMA,
1295
+ partitionKinds: [...CALCTOOL_PARTITION_KINDS],
1296
+ reviewModes: [...CALCTOOL_REVIEW_MODES],
1297
+ maxParallelDefault: 4,
1298
+ },
1299
+ nextStep: blueprintEnabled
1300
+ ? { operation: "blueprint-orchestrate", instruction: "Blueprint collaboration is enabled: call blueprint-orchestrate to plan the tool development, then execute it with the swarm." }
1301
+ : { operation: "brain-invoke", instruction: "Send the requirements with operation brain-invoke to decompose and dispatch the swarm." },
1302
+ });
1303
+ }
1304
+
1305
+ if (operation === "swarm-orchestrate") {
1306
+ const input = request.input ?? {};
1307
+ const requirements = input.requirements ?? {};
1308
+ const plan = decomposeRequirementsToRunPlan(requirements);
1309
+ const errors = validateRunPlan(plan);
1310
+ if (errors.length) {
1311
+ return blockedResponse(requestId, request, errors.map((message, i) => finding("P0", "RUN_PLAN_INVALID", `runPlan[${i}]`, message)));
1312
+ }
1313
+ // 把 calctool 的蜂群任务清单转换为 swarm 项目 JSON(唯一事实源)
1314
+ const project = {
1315
+ schemaVersion: "swarm.project/1.0",
1316
+ name: text(requirements.goal || "calctool-engine"),
1317
+ tasks: plan.tasks.map((task, index) => ({
1318
+ taskId: task.workItemId,
1319
+ title: task.objective,
1320
+ priority: "high",
1321
+ dependsOn: task.dependsOn,
1322
+ })),
1323
+ };
1324
+ return okResponse(requestId, {
1325
+ status: "orchestrated",
1326
+ engineId: plan.engineId,
1327
+ project,
1328
+ swarm: {
1329
+ protocol: "swarm.skill.request/1.0",
1330
+ endpoint: ALLOWED_EXTERNAL_ENDPOINTS.swarm,
1331
+ flow: [
1332
+ { step: "org-chart", note: "大脑按企业组织架构创建 N 个子智能体(board/dispatcher/ops/security-guard/workers)" },
1333
+ { step: "dispatch", note: "把项目 JSON 任务派单给 worker(依赖图驱动)" },
1334
+ { step: "claim", note: "worker 认领任务并执行" },
1335
+ { step: "report", note: "worker 回传结果(进度/产物/错误)" },
1336
+ { step: "traffic-light", note: "每个任务/智能体实时绿/黄/红状态" },
1337
+ { step: "ops", note: "固定运维:心跳检测、回收卡死智能体、派新智能体继承任务续跑" },
1338
+ { step: "security-guard", note: "固定安全守卫:注入/危险指令检测、异常警报" },
1339
+ ],
1340
+ instruction: "POST project to ${ALLOWED_EXTERNAL_ENDPOINTS.swarm} (operation org-chart) with the swarm protocol; the swarm brain dispatches workers by org-chart rules, monitors traffic lights, and Ops/Security Guard take over autonomously.",
1341
+ },
1342
+ nextStep: { operation: "run", instruction: "Feed the swarm project JSON to the swarm runtime; collect worker reports, then merge into the engine definition and run final-gate." },
1343
+ });
1344
+ }
1345
+
1346
+ if (operation === "brain-invoke") {
1347
+ const input = request.input ?? {};
1348
+ const requirements = input.requirements ?? {};
1349
+ const plan = decomposeRequirementsToRunPlan(requirements);
1350
+ const errors = validateRunPlan(plan);
1351
+ if (errors.length) {
1352
+ return blockedResponse(requestId, request, errors.map((message, i) => finding("P0", "RUN_PLAN_INVALID", `runPlan[${i}]`, message)));
1353
+ }
1354
+ const ready = readyTasks(plan, new Set());
1355
+ return okResponse(requestId, {
1356
+ runPlan: plan,
1357
+ dispatch: {
1358
+ totalWorkItems: plan.tasks.length,
1359
+ maxParallel: plan.limits.maxParallel,
1360
+ readyNow: ready.map((t) => ({ workItemId: t.workItemId, role: t.role, objective: t.objective })),
1361
+ instruction: "Dispatch each ready work item to an independent agent (subagent/Hermes). Collect per-item artifacts; when all complete, call brain-complete with the merged artifacts.",
1362
+ },
1363
+ nextStep: { operation: "brain-complete", instruction: "After all work items are dispatched and completed, submit the collected artifacts for deterministic merge." },
1364
+ });
1365
+ }
1366
+
1367
+ if (operation === "brain-events") {
1368
+ const input = request.input ?? {};
1369
+ const events = Array.isArray(input.events) ? input.events : [];
1370
+ return okResponse(requestId, {
1371
+ events,
1372
+ nextStep: { operation: "brain-status", instruction: "Query swarm status or continue dispatching ready work items." },
1373
+ });
1374
+ }
1375
+
1376
+ if (operation === "brain-status") {
1377
+ const input = request.input ?? {};
1378
+ const plan = input.runPlan;
1379
+ const completed = new Set(Array.isArray(input.completedWorkItemIds) ? input.completedWorkItemIds : []);
1380
+ const errors = plan ? validateRunPlan(plan) : [];
1381
+ if (errors.length) {
1382
+ return blockedResponse(requestId, request, errors.map((message, i) => finding("P0", "RUN_PLAN_INVALID", `runPlan[${i}]`, message)));
1383
+ }
1384
+ const ready = plan ? readyTasks(plan, completed) : [];
1385
+ const allDone = plan ? plan.tasks.every((t) => completed.has(t.workItemId)) : false;
1386
+ return okResponse(requestId, {
1387
+ status: allDone ? "complete" : "running",
1388
+ completedWorkItems: [...completed],
1389
+ readyNow: ready.map((t) => t.workItemId),
1390
+ remaining: plan ? plan.tasks.filter((t) => !completed.has(t.workItemId)).length : 0,
1391
+ nextStep: allDone
1392
+ ? { operation: "brain-complete", instruction: "All work items complete; merge the artifacts." }
1393
+ : { operation: "brain-invoke", instruction: "Dispatch the next ready batch." },
1394
+ });
1395
+ }
1396
+
1397
+ if (operation === "brain-complete") {
1398
+ const input = request.input ?? {};
1399
+ const plan = input.runPlan;
1400
+ const artifacts = input.artifacts ?? {};
1401
+ const errors = plan ? validateRunPlan(plan) : ["runPlan is required"];
1402
+ if (errors.length) {
1403
+ return blockedResponse(requestId, request, errors.map((message, i) => finding("P0", "RUN_PLAN_INVALID", `runPlan[${i}]`, message)));
1404
+ }
1405
+ const engine = mergeSwarmArtifacts(plan, artifacts);
1406
+ const findings = validateEngine(engine);
1407
+ if (findings.length) {
1408
+ return blockedResponse(requestId, request, findings);
1409
+ }
1410
+ const digest = createHash("sha256").update(JSON.stringify(engine)).digest("hex");
1411
+ return okResponse(requestId, {
1412
+ status: "complete",
1413
+ revision: 1,
1414
+ swarmProduced: true,
1415
+ runPlanRef: plan.runId,
1416
+ validation: { valid: true, guarantee: "engine-definition-green", findings: [] },
1417
+ artifacts: [{ path: `${engine.engineId}/manifest.yaml`, kind: "engine-manifest", engineId: engine.engineId, digest }],
1418
+ engine,
1419
+ nextStep: { operation: "validate", instruction: "Swarm-generated engine definition is valid; publish it as a versioned engine." },
1420
+ });
1421
+ }
1422
+
1423
+ if (operation === "brain-cancel") {
1424
+ const input = request.input ?? {};
1425
+ return okResponse(requestId, {
1426
+ status: "cancelled",
1427
+ cancelledWorkItemIds: Array.isArray(input.workItemIds) ? input.workItemIds : [],
1428
+ reason: input.reason ?? "cancelled by caller",
1429
+ });
1430
+ }
1431
+
1432
+ return {
1433
+ schemaVersion: RESPONSE_SCHEMA,
1434
+ requestId,
1435
+ status: "failed",
1436
+ errorSchema: ERROR_SCHEMA,
1437
+ error: { code: "UNSUPPORTED_OPERATION", message: `Unsupported operation: ${operation}` },
1438
+ };
1439
+ }
1440
+
1441
+ return { run, validateRequest, blockedResponse, validateRunPlan, mergeSwarmArtifacts, decomposeRequirementsToRunPlan, readyTasks, probeEnvironment, adaptEnvironment };
1442
+ })();
1443
+
1444
+ const DECIMAL_SCALE = 28;
1445
+ const DECIMAL_POLICY = Object.freeze({ precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' });
1446
+ const LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
1447
+ const MAX_DIGITS = 4096;
1448
+
1449
+ function formulaError(code, message, details = {}) {
1450
+ return Object.assign(new Error(message), { code, ...details });
1451
+ }
1452
+ function scale(value, maximum = DECIMAL_SCALE) {
1453
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) throw formulaError('DECIMAL_SCALE_INVALID', `decimal places must be 0..${maximum}`);
1454
+ return value;
1455
+ }
1456
+ function ten(power) { return 10n ** BigInt(scale(power, MAX_DIGITS * 2)); }
1457
+ function normalize(coefficient, places) {
1458
+ while (places > 0 && coefficient % 10n === 0n) { coefficient /= 10n; places -= 1; }
1459
+ return { coefficient, scale: places };
1460
+ }
1461
+ function rounded(coefficient, fromScale, toScale) {
1462
+ if (fromScale <= toScale) return coefficient * ten(toScale - fromScale);
1463
+ const divisor = ten(fromScale - toScale);
1464
+ let quotient = coefficient / divisor;
1465
+ const remainder = coefficient % divisor;
1466
+ if ((remainder < 0n ? -remainder : remainder) * 2n >= divisor) quotient += coefficient < 0n ? -1n : 1n;
1467
+ return quotient;
1468
+ }
1469
+ function requireCoefficient(value) {
1470
+ if (typeof value !== 'bigint') throw formulaError('INVALID_DECIMAL', 'decimal coefficient must be a bigint');
1471
+ if (value.toString().replace('-', '').length > MAX_DIGITS) throw formulaError('DECIMAL_LIMIT', 'decimal exceeds the supported digit limit');
1472
+ }
1473
+
1474
+ class Decimal {
1475
+ constructor(value) {
1476
+ if (value instanceof Decimal) { this.coefficient = value.coefficient; this.scale = value.scale; }
1477
+ else {
1478
+ if (!['string', 'number', 'bigint'].includes(typeof value) || (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))) {
1479
+ throw formulaError('INVALID_DECIMAL', 'decimal must be a finite, lossless number or decimal string');
1480
+ }
1481
+ const source = String(value).trim();
1482
+ if (source.length > MAX_DIGITS || !LITERAL.test(source)) throw formulaError('INVALID_DECIMAL', 'invalid decimal literal');
1483
+ const negative = source[0] === '-';
1484
+ const [integer = '0', fraction = ''] = source.replace(/^[+-]/, '').split('.');
1485
+ const coefficient = BigInt(`${integer || '0'}${fraction}`) * (negative ? -1n : 1n);
1486
+ Object.assign(this, normalize(rounded(coefficient, fraction.length, Math.min(fraction.length, DECIMAL_SCALE)), Math.min(fraction.length, DECIMAL_SCALE)));
1487
+ }
1488
+ this.value = Decimal.format(this.coefficient, this.scale);
1489
+ this.s = this.value;
1490
+ Object.freeze(this);
1491
+ }
1492
+ static from(value) { return value instanceof Decimal ? value : new Decimal(value); }
1493
+ static fromParts(coefficient, places) {
1494
+ requireCoefficient(coefficient); scale(places, MAX_DIGITS);
1495
+ const boundedScale = Math.min(places, DECIMAL_SCALE);
1496
+ const parts = normalize(rounded(coefficient, places, boundedScale), boundedScale);
1497
+ return new Decimal(Decimal.format(parts.coefficient, parts.scale));
1498
+ }
1499
+ static format(coefficient, places) {
1500
+ requireCoefficient(coefficient); scale(places, MAX_DIGITS);
1501
+ const digits = (coefficient < 0n ? -coefficient : coefficient).toString().padStart(places + 1, '0');
1502
+ const value = places === 0 ? digits : `${digits.slice(0, -places)}.${digits.slice(-places)}`;
1503
+ return coefficient < 0n ? `-${value}` : value;
1504
+ }
1505
+ plus(other) {
1506
+ const right = Decimal.from(other); const places = Math.max(this.scale, right.scale);
1507
+ return Decimal.fromParts(this.coefficient * ten(places - this.scale) + right.coefficient * ten(places - right.scale), places);
1508
+ }
1509
+ minus(other) { return this.plus(Decimal.from(other).negate()); }
1510
+ negate() { return Decimal.fromParts(-this.coefficient, this.scale); }
1511
+ times(other) { const right = Decimal.from(other); return Decimal.fromParts(this.coefficient * right.coefficient, this.scale + right.scale); }
1512
+ div(other) {
1513
+ const right = Decimal.from(other);
1514
+ if (right.isZero()) throw formulaError('DIV_ZERO', 'division by zero', { nodeId: 'div' });
1515
+ const numerator = this.coefficient * ten(right.scale + DECIMAL_SCALE);
1516
+ const denominator = right.coefficient * ten(this.scale);
1517
+ let quotient = numerator / denominator;
1518
+ const remainder = numerator % denominator;
1519
+ if ((remainder < 0n ? -remainder : remainder) * 2n >= (denominator < 0n ? -denominator : denominator)) quotient += (numerator < 0n) !== (denominator < 0n) ? -1n : 1n;
1520
+ return Decimal.fromParts(quotient, DECIMAL_SCALE);
1521
+ }
1522
+ rounded(dp) { scale(dp); return this.scale <= dp ? this : Decimal.fromParts(rounded(this.coefficient, this.scale, dp), dp); }
1523
+ toDecimalPlaces(dp) { return this.rounded(dp); }
1524
+ isZero() { return this.coefficient === 0n; }
1525
+ isNegative() { return this.coefficient < 0n; }
1526
+ toString() { return this.value; }
1527
+ }
1528
+ const Decimal28 = Decimal;
1529
+
1530
+ const FORMULA_OPERATORS = Object.freeze(['add', 'sub', 'mul', 'div', 'safeDivide', 'percentOf', 'round', 'if']);
1531
+ const ARITY = { add: [0, 1000], mul: [0, 1000], sub: [2, 2], div: [2, 2], safeDivide: [2, 2], percentOf: [2, 2], round: [1, 1], if: [3, 3] };
1532
+ function formulaKey(value) {
1533
+ if (typeof value !== 'string' || !value.trim() || value.length > 128 || /[\u0000-\u001f\u007f]/.test(value)) throw formulaError('FORMULA_KEY_INVALID', 'formula and field keys must be non-empty strings of at most 128 characters');
1534
+ return value;
1535
+ }
1536
+ function formulaObject(value, field) {
1537
+ if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw formulaError('FORMULA_NODE_INVALID', `${field} must be a plain object`);
1538
+ }
1539
+ function normalizeFormula(node, budget = { nodes: 0, active: new Set() }, depth = 0) {
1540
+ formulaObject(node, 'formula node');
1541
+ if (depth > 64 || ++budget.nodes > 10000) throw formulaError('FORMULA_LIMIT', 'formula exceeds its depth or node limit');
1542
+ if (budget.active.has(node)) throw formulaError('FORMULA_CYCLE', 'formula AST contains an object cycle');
1543
+ const keys = Object.keys(node);
1544
+ if (keys.length === 1 && Object.hasOwn(node, 'ref')) return { ref: formulaKey(node.ref) };
1545
+ if (keys.length === 1 && Object.hasOwn(node, 'lit')) return { lit: Decimal.from(node.lit).toString() };
1546
+ if (!Object.hasOwn(ARITY, node.op)) throw formulaError('UNSUPPORTED_OPERATOR', `Unsupported operator: ${String(node.op)}`);
1547
+ if (keys.length !== 2 || !keys.includes('args') || !keys.includes('op')) throw formulaError('FORMULA_NODE_INVALID', 'operator nodes contain only op and args');
1548
+ const [minimum, maximum] = ARITY[node.op];
1549
+ if (!Array.isArray(node.args) || node.args.length < minimum || node.args.length > maximum || node.args.some((_, i) => !Object.hasOwn(node.args, i))) throw formulaError('FORMULA_ARGS_INVALID', `${node.op} requires ${minimum === maximum ? minimum : `${minimum}..${maximum}`} arguments`);
1550
+ budget.active.add(node);
1551
+ const args = Array.from(node.args, item => normalizeFormula(item, budget, depth + 1));
1552
+ budget.active.delete(node);
1553
+ return { op: node.op, args };
1554
+ }
1555
+ function formulaRefs(node, output = new Set()) {
1556
+ if (Object.hasOwn(node, 'ref')) output.add(node.ref);
1557
+ if (node.args) for (const item of node.args) formulaRefs(item, output);
1558
+ return [...output].sort();
1559
+ }
1560
+ function evaluateNode(node, resolve) {
1561
+ if (Object.hasOwn(node, 'lit')) return Decimal.from(node.lit);
1562
+ if (Object.hasOwn(node, 'ref')) return Decimal.from(resolve(node.ref));
1563
+ const [left, right, alternate] = node.args;
1564
+ switch (node.op) {
1565
+ case 'add': return node.args.reduce((total, item) => total.plus(evaluateNode(item, resolve)), Decimal.from('0'));
1566
+ case 'mul': return node.args.reduce((total, item) => total.times(evaluateNode(item, resolve)), Decimal.from('1'));
1567
+ case 'sub': return evaluateNode(left, resolve).minus(evaluateNode(right, resolve));
1568
+ case 'div': return evaluateNode(left, resolve).div(evaluateNode(right, resolve));
1569
+ case 'safeDivide': { const denominator = evaluateNode(right, resolve); return denominator.isZero() ? Decimal.from('0') : evaluateNode(left, resolve).div(denominator); }
1570
+ case 'percentOf': return evaluateNode(left, resolve).div(evaluateNode(right, resolve)).times('100');
1571
+ case 'round': return evaluateNode(left, resolve).rounded(2);
1572
+ case 'if': return evaluateNode(left, resolve).isZero() ? evaluateNode(alternate, resolve) : evaluateNode(right, resolve);
1573
+ default: throw formulaError('UNSUPPORTED_OPERATOR', 'unsupported normalized formula operator');
1574
+ }
1575
+ }
1576
+ function evaluateFormula(node, values = {}) {
1577
+ formulaObject(values, 'formula values');
1578
+ return evaluateNode(normalizeFormula(node), key => {
1579
+ if (!Object.hasOwn(values, key) || values[key] === null || values[key] === undefined || values[key] === '') throw formulaError('MISSING_INPUT', `missing input: ${key}`, { fieldId: key });
1580
+ return values[key];
1581
+ });
1582
+ }
1583
+ function compileFormulaGraph(formulas, fieldKeys) {
1584
+ if (!Array.isArray(formulas) || formulas.length > 1000) throw formulaError('FORMULA_GRAPH_INVALID', 'formulas must be an array of at most 1000 definitions');
1585
+ if (fieldKeys !== undefined && !Array.isArray(fieldKeys)) throw formulaError('FORMULA_GRAPH_INVALID', 'fieldKeys must be an array');
1586
+ const fields = fieldKeys === undefined ? undefined : new Set(fieldKeys.map(formulaKey));
1587
+ const definitions = new Map(); const budget = { nodes: 0, active: new Set() };
1588
+ for (const item of formulas) {
1589
+ formulaObject(item, 'formula definition'); const key = formulaKey(item.key);
1590
+ if (definitions.has(key) || fields?.has(key)) throw formulaError('FORMULA_KEY_INVALID', 'formula keys must be unique and separate from input fields');
1591
+ definitions.set(key, { key, expression: normalizeFormula(item.expression, budget) });
1592
+ }
1593
+ const dependencies = Object.fromEntries([...definitions].map(([key, item]) => [key, formulaRefs(item.expression)]));
1594
+ const visiting = new Set(); const done = new Set(); const order = [];
1595
+ const visit = key => {
1596
+ if (done.has(key)) return;
1597
+ if (visiting.has(key)) throw formulaError('FORMULA_CYCLE', `formula cycle at ${key}`);
1598
+ visiting.add(key);
1599
+ for (const ref of dependencies[key]) {
1600
+ if (definitions.has(ref)) visit(ref);
1601
+ else if (fields && !fields.has(ref)) throw formulaError('FORMULA_REF_UNKNOWN', `unknown formula reference: ${ref}`);
1602
+ }
1603
+ visiting.delete(key); done.add(key); order.push(key);
1604
+ };
1605
+ for (const key of [...definitions.keys()].sort()) visit(key);
1606
+ return { formulas: [...definitions.values()].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0), dependencies, order };
1607
+ }
1608
+ function evaluateFormulaGraph(formulas, inputs = {}) {
1609
+ formulaObject(inputs, 'formula inputs');
1610
+ const values = new Map(Object.entries(inputs).map(([key, value]) => [key, Decimal.from(value)]));
1611
+ const graph = compileFormulaGraph(formulas);
1612
+ const definitions = new Map(graph.formulas.map(item => [item.key, item.expression]));
1613
+ const results = new Map(); const visiting = new Set();
1614
+ const resolve = key => {
1615
+ if (results.has(key)) return results.get(key);
1616
+ if (!definitions.has(key)) {
1617
+ if (!values.has(key)) throw formulaError('MISSING_INPUT', `missing input: ${key}`, { fieldId: key });
1618
+ return values.get(key);
1619
+ }
1620
+ if (visiting.has(key)) throw formulaError('FORMULA_CYCLE', `formula cycle at ${key}`);
1621
+ visiting.add(key); const value = evaluateNode(definitions.get(key), resolve); visiting.delete(key); results.set(key, value); return value;
1622
+ };
1623
+ return Object.fromEntries(graph.formulas.map(item => [item.key, resolve(item.key).toString()]));
1624
+ }
1625
+
1626
+ const FORMULA_SCHEMA = 'entrorove.formula-engine/1.0';
1627
+ const FORMULA_RUNTIME_VERSION = 'decimal28-ast/1.0';
1628
+ const TYPES = ['number', 'integer', 'money', 'percent', 'text', 'date', 'enum'];
1629
+ const NUMERIC = TYPES.slice(0, 4);
1630
+ const ID = /^[a-z0-9][a-z0-9._-]{0,127}$/;
1631
+ const VERSION = /^\d+\.\d+\.\d+$/;
1632
+
1633
+ function formulaCanonical(value) {
1634
+ if (Array.isArray(value)) return `[${value.map(formulaCanonical).join(',')}]`;
1635
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${formulaCanonical(value[key])}`).join(',')}}`;
1636
+ const json = JSON.stringify(value);
1637
+ if (json === undefined || (typeof value === 'number' && !Number.isFinite(value))) throw formulaError('FORMULA_JSON_INVALID', 'formula artifacts must contain finite JSON values');
1638
+ return json;
1639
+ }
1640
+ const formulaDigest = value => createHash('sha256').update(typeof value === 'string' || ArrayBuffer.isView(value) ? value : formulaCanonical(value)).digest('hex');
1641
+ function cloned(value) { return JSON.parse(formulaCanonical(value)); }
1642
+ function requiredText(value, field) {
1643
+ if (typeof value !== 'string' || !value.trim()) throw formulaError('ENGINE_REQUIRED_FIELD', `${field} is required`);
1644
+ }
1645
+ function normalizeEngine(engine) {
1646
+ formulaObject(engine, 'engine');
1647
+ if (engine.schemaVersion !== 'engine.spec/1') throw formulaError('ENGINE_SCHEMA_VERSION', 'Expected engine.spec/1');
1648
+ if (typeof engine.engineId !== 'string' || !ID.test(engine.engineId)) throw formulaError('ENGINE_ID_INVALID', 'engineId must be an explicit stable lowercase identifier');
1649
+ requiredText(engine.name, 'name');
1650
+ if (typeof engine.semanticVersion !== 'string' || !VERSION.test(engine.semanticVersion)) throw formulaError('ENGINE_VERSION_INVALID', 'semanticVersion must be major.minor.patch');
1651
+ if (engine.decimalPolicy !== 'decimal-string' && formulaCanonical(engine.decimalPolicy ?? null) !== formulaCanonical(DECIMAL_POLICY)) throw formulaError('DECIMAL_POLICY_INVALID', 'engine requires Decimal28 decimal-string with half-away-from-zero rounding');
1652
+ for (const field of ['fields', 'formulas', 'rules', 'views', 'testSuites', 'importProfiles']) {
1653
+ if (!Array.isArray(engine[field])) throw formulaError('ENGINE_REQUIRED_ARRAY', `${field} must be an array`);
1654
+ }
1655
+ if (!engine.fields.length || !engine.formulas.length || engine.fields.length > 1000) throw formulaError('ENGINE_EMPTY', 'engine requires 1..1000 fields and 1..1000 formulas');
1656
+ const keys = new Set();
1657
+ for (const field of engine.fields) {
1658
+ formulaObject(field, 'field'); const key = formulaKey(field.key);
1659
+ if (keys.has(key)) throw formulaError('FIELD_DUPLICATE_KEY', `duplicate field key ${key}`);
1660
+ keys.add(key); requiredText(field.label, 'field label');
1661
+ if (!TYPES.includes(field.type)) throw formulaError('FIELD_TYPE_INVALID', 'unsupported field type');
1662
+ if (NUMERIC.includes(field.type) && typeof field.unit !== 'string') throw formulaError('FIELD_UNIT_MISSING', 'numeric fields require an explicit unit');
1663
+ if (field.required !== undefined && typeof field.required !== 'boolean') throw formulaError('FIELD_REQUIRED_INVALID', 'field required must be boolean');
1664
+ }
1665
+ const graph = compileFormulaGraph(engine.formulas, [...keys]);
1666
+ for (const item of graph.formulas) for (const ref of graph.dependencies[item.key]) {
1667
+ const field = engine.fields.find(entry => entry.key === ref);
1668
+ if (field && !NUMERIC.includes(field.type)) throw formulaError('FORMULA_REF_TYPE', `formula reference ${ref} must be numeric`);
1669
+ }
1670
+ const normalized = cloned(engine);
1671
+ normalized.formulas = engine.formulas.map(item => ({ ...cloned(item), expression: graph.formulas.find(entry => entry.key === item.key).expression }));
1672
+ return { engine: normalized, graph };
1673
+ }
1674
+ function validateFormulaEngine(engine) {
1675
+ try { normalizeEngine(engine); return []; }
1676
+ catch (cause) { return [{ severity: 'P0', ruleId: cause.code ?? 'ENGINE_INVALID', entityRef: 'engine', message: cause.message }]; }
1677
+ }
1678
+ function buildFormulaEngine(requirements) {
1679
+ formulaObject(requirements, 'requirements'); requiredText(requirements.goal, 'goal');
1680
+ if (!Array.isArray(requirements.inputs) || !Array.isArray(requirements.formulas)) throw formulaError('COMPILE_REQUIREMENTS', 'inputs and formulas are required arrays');
1681
+ const engine = {
1682
+ schemaVersion: 'engine.spec/1', engineId: requirements.engineId, name: requirements.name ?? requirements.goal,
1683
+ semanticVersion: requirements.semanticVersion ?? '1.0.0', status: 'draft', compatibilityProfile: 'legacy-compatible',
1684
+ decimalPolicy: requirements.decimalPolicy ?? 'decimal-string', defaultLocale: requirements.defaultLocale ?? 'zh-CN',
1685
+ category: requirements.category ?? 'general', inputMethod: requirements.inputMethod ?? 'manual', output: requirements.output ?? ['metric-cards'],
1686
+ constraints: requirements.constraints ?? '', fields: requirements.inputs, formulas: requirements.formulas,
1687
+ rules: requirements.rules ?? [], views: requirements.views ?? [], importProfiles: requirements.importProfiles ?? [],
1688
+ reports: requirements.reports ?? [], testSuites: requirements.testSuites ?? [], acceptance: requirements.acceptance ?? '',
1689
+ };
1690
+ return normalizeEngine(engine).engine;
1691
+ }
1692
+ function compileFormulaEngine(engine) {
1693
+ const normalized = normalizeEngine(engine);
1694
+ const program = { schemaVersion: FORMULA_SCHEMA, runtimeVersion: FORMULA_RUNTIME_VERSION, decimalPolicy: DECIMAL_POLICY,
1695
+ engineId: normalized.engine.engineId, engineVersion: normalized.engine.semanticVersion,
1696
+ engineDigest: formulaDigest(normalized.engine), engine: normalized.engine,
1697
+ formulas: normalized.graph.formulas, dependencies: normalized.graph.dependencies, order: normalized.graph.order };
1698
+ return { ...program, programDigest: formulaDigest(program) };
1699
+ }
1700
+ function verifyFormulaProgram(program, expected = {}) {
1701
+ formulaObject(program, 'compiled formula program');
1702
+ if (program.runtimeVersion !== FORMULA_RUNTIME_VERSION || program.schemaVersion !== FORMULA_SCHEMA) throw formulaError('FORMULA_RUNTIME_VERSION', 'compiled formula runtime version is unsupported');
1703
+ const compiled = compileFormulaEngine(program.engine);
1704
+ if (formulaCanonical(compiled) !== formulaCanonical(program)) throw formulaError('FORMULA_ARTIFACT_BINDING', 'compiled formula artifact or digest does not match its engine');
1705
+ for (const field of ['engineDigest', 'programDigest', 'engineId', 'engineVersion']) {
1706
+ if (expected[field] !== undefined && expected[field] !== compiled[field]) throw formulaError('FORMULA_ARTIFACT_BINDING', `${field} does not match the required engine binding`);
1707
+ }
1708
+ return compiled;
1709
+ }
1710
+ function evaluateFormulaProgram(program, inputs = {}, expected = {}) {
1711
+ const verified = verifyFormulaProgram(program, expected); formulaObject(inputs, 'engine inputs');
1712
+ const fields = new Map(verified.engine.fields.map(item => [item.key, item]));
1713
+ for (const [key, value] of Object.entries(inputs)) {
1714
+ const field = fields.get(key);
1715
+ if (!field) throw formulaError('ENGINE_INPUT_UNKNOWN', `unknown input field: ${key}`);
1716
+ if (NUMERIC.includes(field.type)) {
1717
+ const decimal = Decimal.from(value);
1718
+ if (field.type === 'integer' && decimal.scale !== 0) throw formulaError('ENGINE_INPUT_INTEGER', `${key} must be an integer`);
1719
+ } else if (typeof value !== 'string') throw formulaError('ENGINE_INPUT_TYPE', `${key} must be text`);
1720
+ }
1721
+ for (const field of fields.values()) if (field.required && (!Object.hasOwn(inputs, field.key) || inputs[field.key] === '' || inputs[field.key] === null)) throw formulaError('MISSING_INPUT', `missing input: ${field.key}`, { fieldId: field.key });
1722
+ return evaluateFormulaGraph(verified.formulas, Object.fromEntries(Object.entries(inputs).filter(([key]) => NUMERIC.includes(fields.get(key).type))));
1723
+ }
1724
+ function formulaImpact(program, changedKeys) {
1725
+ const verified = verifyFormulaProgram(program);
1726
+ if (!Array.isArray(changedKeys) || !changedKeys.length) throw formulaError('FORMULA_IMPACT_INVALID', 'changedKeys must be a non-empty array');
1727
+ const known = new Set([...verified.engine.fields.map(item => item.key), ...verified.formulas.map(item => item.key)]);
1728
+ const affected = new Set(changedKeys.map(formulaKey));
1729
+ for (const key of affected) if (!known.has(key)) throw formulaError('FORMULA_REF_UNKNOWN', `unknown changed key: ${key}`);
1730
+ for (const key of verified.order) if (verified.dependencies[key].some(ref => affected.has(ref))) affected.add(key);
1731
+ return verified.order.filter(key => affected.has(key));
1732
+ }
1733
+
1734
+ const FORMULA_GATE_SCHEMA = 'entrorove.calctool-final-gate/1.0';
1735
+
1736
+ function suiteValue(suite, primary, alias) {
1737
+ if (Object.hasOwn(suite, primary) && Object.hasOwn(suite, alias) && formulaCanonical(suite[primary]) !== formulaCanonical(suite[alias])) throw formulaError('FORMULA_SUITE_ALIAS', `conflicting ${primary}/${alias} test values`);
1738
+ const value = suite[primary] ?? suite[alias]; formulaObject(value, `test ${primary}`); return value;
1739
+ }
1740
+ function runSuite(program, suite, index) {
1741
+ formulaObject(suite, 'test suite');
1742
+ const expected = suiteValue(suite, 'expected', 'expect');
1743
+ const inputs = suiteValue(suite, 'input', 'inputs');
1744
+ const keys = Object.keys(expected);
1745
+ if (!keys.length) throw formulaError('FORMULA_TEST_EMPTY', 'test suites must assert at least one formula result');
1746
+ const known = new Set(program.formulas.map(item => item.key));
1747
+ for (const key of keys) if (!known.has(key)) throw formulaError('FORMULA_TEST_KEY_UNKNOWN', `test expectation references unknown formula: ${key}`);
1748
+ const actual = evaluateFormulaProgram(program, inputs);
1749
+ const mismatches = keys.flatMap(key => {
1750
+ const want = Decimal.from(expected[key]).toString();
1751
+ return actual[key] === want ? [] : [{ key, expected: want, actual: actual[key] }];
1752
+ });
1753
+ return { name: suite.name ?? `suite-${index + 1}`, passed: mismatches.length === 0, keys, actual, mismatches };
1754
+ }
1755
+ function formulaFinalGate(engine, options = {}) {
1756
+ const started = Date.now(); const findings = []; const tests = []; let program;
1757
+ try { program = compileFormulaEngine(engine); }
1758
+ catch (cause) { findings.push({ severity: 'P0', ruleId: cause.code ?? 'ENGINE_INVALID', entityRef: 'engine', message: cause.message }); }
1759
+ if (program) {
1760
+ const suites = program.engine.testSuites;
1761
+ if (!suites.length) findings.push({ severity: 'P1', ruleId: 'FORMULA_TESTS_EMPTY', entityRef: 'testSuites', message: 'no runnable benchmark samples' });
1762
+ for (const [index, suite] of suites.entries()) {
1763
+ try {
1764
+ const result = runSuite(program, suite, index); tests.push(result);
1765
+ if (!result.passed) findings.push({ severity: 'P1', ruleId: 'FORMULA_TEST_FAIL', entityRef: `testSuites[${index}]`, message: 'formula test expectation mismatch', mismatches: result.mismatches });
1766
+ } catch (cause) {
1767
+ tests.push({ name: suite?.name ?? `suite-${index + 1}`, passed: false, code: cause.code ?? 'FORMULA_TEST_ERROR', message: cause.message });
1768
+ findings.push({ severity: 'P1', ruleId: cause.code ?? 'FORMULA_TEST_ERROR', entityRef: `testSuites[${index}]`, message: cause.message });
1769
+ }
1770
+ }
1771
+ const coverage = new Set(tests.filter(item => item.passed).flatMap(item => item.keys));
1772
+ for (const formula of program.formulas) if (!coverage.has(formula.key)) findings.push({ severity: 'P1', ruleId: 'FORMULA_TEST_UNCOVERED', entityRef: formula.key, message: 'formula has no passing expectation' });
1773
+ }
1774
+ const passed = findings.length === 0;
1775
+ const artifact = { schemaVersion: 'entrorove.formula-test-result/1.0', programDigest: program?.programDigest ?? null, findings, tests };
1776
+ const artifactSha256 = formulaDigest(artifact);
1777
+ const subject = { schemaVersion: 'entrorove.formula-execution-subject/1.0', engineId: engine?.engineId ?? null,
1778
+ engineVersion: program?.engineVersion ?? null, runtimeVersion: program?.runtimeVersion ?? null,
1779
+ engineDigest: program?.engineDigest ?? null, programDigest: program?.programDigest ?? null, artifactSha256,
1780
+ flowId: options.flowId ?? null, stepId: options.stepId ?? null };
1781
+ const evidence = { schemaVersion: 'entrorove.test-evidence/2.0', evidenceId: `formula-${artifactSha256}`,
1782
+ kind: 'test', runner: 'local', command: 'entrorove local formula-final-gate', exitCode: passed ? 0 : 2,
1783
+ durationMs: Math.max(0, Date.now() - started), summary: `${tests.filter(item => item.passed).length}/${tests.length} formula suites passed`,
1784
+ artifactSha256, subject, subjectDigest: formulaDigest(subject),
1785
+ ...(options.flowId ? { flowId: options.flowId } : {}), ...(options.stepId ? { stepId: options.stepId } : {}) };
1786
+ return { schemaVersion: FORMULA_GATE_SCHEMA, status: passed ? 'incomplete' : 'blocked', authority: 'local',
1787
+ computationsPassed: passed, findings, tests, engineId: engine?.engineId ?? null, artifact, evidence: [evidence],
1788
+ ...(program ? { programDigest: program.programDigest, engineDigest: program.engineDigest, engineVersion: program.engineVersion, runtimeVersion: program.runtimeVersion } : {}),
1789
+ guarantee: passed ? 'formula-tests-passed-awaiting-independent-validator' : 'formula-tests-blocked' };
1790
+ }
1791
+ function verifyFormulaTests(program, options = {}) { return formulaFinalGate(verifyFormulaProgram(program).engine, options); }
1792
+
1793
+ const CLOUD_OPERATIONS = ['capabilities', 'help', 'intake', 'validate', 'compile-inline'];
1794
+ const LOCAL_OPERATIONS = ['run', 'compile', 'generate', 'verify', 'estimate', 'impact', 'status', 'inventory', 'purge',
1795
+ 'brain-handshake', 'brain-invoke', 'brain-events', 'brain-cancel', 'brain-complete', 'brain-resume', 'brain-status',
1796
+ 'probe-env', 'adapt-config', 'intake-round', 'compile-tool', 'blueprint-orchestrate', 'auto-pipeline', 'final-gate', 'swarm-orchestrate'];
1797
+ function failure(request, cause) {
1798
+ return { schemaVersion: 'calctool.skill.response/1.0', requestId: request?.requestId ?? 'unknown', status: 'blocked',
1799
+ validation: { valid: false, guarantee: 'blocked', findings: [{ severity: 'P0', ruleId: cause.code ?? 'FORMULA_INVALID', entityRef: 'input', message: cause.message }] } };
1800
+ }
1801
+ function compiled(request, engine) {
1802
+ const program = compileFormulaEngine(engine);
1803
+ return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId, status: 'succeeded', state: 'reported',
1804
+ revision: program.engineVersion, engine: program.engine, program,
1805
+ validation: { valid: true, guarantee: 'formula-compiled', findings: [] },
1806
+ artifacts: [{ path: `${program.engineId}/engine.json`, kind: 'engine-definition', engineId: program.engineId,
1807
+ engineVersion: program.engineVersion, digest: program.engineDigest },
1808
+ { path: `${program.engineId}/program.json`, kind: 'formula-program', engineId: program.engineId,
1809
+ engineVersion: program.engineVersion, runtimeVersion: program.runtimeVersion, digest: program.programDigest }],
1810
+ nextStep: { operation: 'calctool-verify', instruction: 'Execute the compiled engine benchmark samples locally, then submit bound evidence to the independent validator.' } };
1811
+ }
1812
+ function createCalctoolEntrypoint(core) {
1813
+ return async function run(request, options = {}) {
1814
+ const requestFindings = core.validateRequest(request);
1815
+ if (requestFindings.length) return core.blockedResponse(request?.requestId ?? 'unknown', request, requestFindings);
1816
+ try {
1817
+ if (request.operation === 'capabilities') {
1818
+ const output = await core.run(request, options);
1819
+ return { ...output, capabilities: { ...output.capabilities, operations: CLOUD_OPERATIONS, localRunnerOperations: LOCAL_OPERATIONS,
1820
+ formulaOperators: FORMULA_OPERATORS, engineSchema: 'engine.spec/1', formulaRuntime: 'decimal28-ast/1.0',
1821
+ decimalPolicy: { precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' } },
1822
+ localRunner: { command: 'entrorove local calctool-<operation>', requiredFor: LOCAL_OPERATIONS } };
1823
+ }
1824
+ if (request.operation === 'validate') {
1825
+ const findings = validateFormulaEngine(request.input?.engine);
1826
+ if (findings.length) return core.blockedResponse(request.requestId, request, findings);
1827
+ return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId, status: 'succeeded', state: 'reported',
1828
+ validation: { valid: true, guarantee: 'formula-definition-valid', findings: [] } };
1829
+ }
1830
+ if (request.operation === 'compile-inline') return compiled(request, buildFormulaEngine(request.input?.requirements));
1831
+ if (LOCAL_OPERATIONS.includes(request.operation)) return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId,
1832
+ status: 'incomplete', validation: { valid: false, guarantee: 'local-runner-required', findings: [] },
1833
+ localRunner: { required: true, operation: `calctool-${request.operation}`, command: `entrorove local calctool-${request.operation}` } };
1834
+ return core.run(request, options);
1835
+ } catch (cause) { return failure(request, cause); }
1836
+ };
1837
+ }
1838
+
1839
+ function resumeCalctoolRun(core, request) {
1840
+ const input = request.input; const plan = input.runPlan;
1841
+ const errors = core.validateRunPlan(plan);
1842
+ if (errors.length) throw formulaError('RUN_PLAN_INVALID', errors.join('; '));
1843
+ const known = new Set(plan.tasks.map(task => task.workItemId)); const seen = new Set();
1844
+ const groups = ['completedWorkItemIds', 'inFlightWorkItemIds', 'uncertainWorkItemIds'].map(field => {
1845
+ const ids = input[field] ?? [];
1846
+ if (!Array.isArray(ids)) throw formulaError('RUN_RESUME_INVALID', 'resume checkpoints must contain work item arrays');
1847
+ for (const id of ids) {
1848
+ if (!known.has(id) || seen.has(id)) throw formulaError('RUN_RESUME_INVALID', 'resume checkpoint contains an unknown, duplicate or conflicting work item');
1849
+ seen.add(id);
1850
+ }
1851
+ return new Set(ids);
1852
+ });
1853
+ const [completed, inFlight, uncertain] = groups;
1854
+ for (const task of plan.tasks) if ((completed.has(task.workItemId) || inFlight.has(task.workItemId))
1855
+ && task.dependsOn.some(id => !completed.has(id))) throw formulaError('RUN_RESUME_INVALID', 'completed or executing work items require completed dependencies');
1856
+ const availableSlots = Math.max(0, plan.limits.maxParallel - inFlight.size - uncertain.size);
1857
+ const ready = uncertain.size ? [] : core.readyTasks(plan, completed).filter(task => !inFlight.has(task.workItemId));
1858
+ const dispatchable = ready.slice(0, Math.min(availableSlots, plan.limits.maxReadyPageSize));
1859
+ const ordered = group => plan.tasks.filter(task => group.has(task.workItemId)).map(task => task.workItemId);
1860
+ const checkpoint = { completedWorkItemIds: ordered(completed), inFlightWorkItemIds: ordered(inFlight), uncertainWorkItemIds: ordered(uncertain) };
1861
+ const remaining = plan.tasks.filter(task => !completed.has(task.workItemId));
1862
+ return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId, status: uncertain.size ? 'incomplete' : 'reported',
1863
+ resumed: true, executed: false, runPlan: plan, runPlanRef: plan.runId, resumeDigest: formulaDigest({ runPlan: plan, checkpoint }), checkpoint,
1864
+ completedWorkItems: checkpoint.completedWorkItemIds, readyNow: dispatchable.map(task => task.workItemId), remaining: remaining.length,
1865
+ pendingWorkItems: remaining, dispatch: { maxParallel: plan.limits.maxParallel, availableSlots, readyNow: dispatchable },
1866
+ reconciliation: { required: uncertain.size > 0, workItemIds: checkpoint.uncertainWorkItemIds },
1867
+ nextStep: remaining.length === 0 ? { operation: 'brain-complete', instruction: 'Merge the original run artifacts.' }
1868
+ : uncertain.size ? { operation: 'brain-events', instruction: 'Reconcile original work item results before dispatching any continuation.' }
1869
+ : { operation: 'brain-events', instruction: 'Continue only the returned dispatch tasks in the original run and record their results.' } };
1870
+ }
1871
+ async function runCalctoolCoreOperation(core, operation, input) {
1872
+ const request = { schemaVersion: 'calctool.skill.request/1.0', requestId: 'local-calctool', operation, input };
1873
+ if (operation === 'brain-resume') return resumeCalctoolRun(core, request);
1874
+ if (operation === 'brain-complete') {
1875
+ const errors = core.validateRunPlan(input.runPlan);
1876
+ if (errors.length) return core.blockedResponse(request.requestId, request, errors.map(message => ({ severity: 'P0', ruleId: 'RUN_PLAN_INVALID', entityRef: 'input.runPlan', message })));
1877
+ return compiled(request, core.mergeSwarmArtifacts(input.runPlan, input.artifacts));
1878
+ }
1879
+ return core.run(request);
1880
+ }
1881
+
1882
+ export const run = createCalctoolEntrypoint(calctoolCore);
1883
+ export const runLocalCore = (operation, input) => runCalctoolCoreOperation(calctoolCore, operation, input);
1884
+ export const probeLocalEnvironment = calctoolCore.probeEnvironment;
1885
+ export const adaptLocalEnvironment = calctoolCore.adaptEnvironment;
1886
+ export const evaluateCompiledProgram = evaluateFormulaProgram;
1887
+ export const verifyCompiledProgram = verifyFormulaTests;