whale-code 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -0,0 +1,867 @@
1
+ // server/lib/pdf-renderer.ts — PDF template rendering engine
2
+ // Processes pdf_templates layout → HTML, with generation rules, calculations, and validation.
3
+ import { fillTemplate } from "./utils.js";
4
+ function resolvePattern(pattern) {
5
+ const now = new Date();
6
+ return pattern
7
+ .replace("{YYYYMMDD}", now.toISOString().slice(0, 10).replace(/-/g, ""))
8
+ .replace("{YYYY}", String(now.getFullYear()))
9
+ .replace("{MM}", String(now.getMonth() + 1).padStart(2, "0"))
10
+ .replace("{DD}", String(now.getDate()).padStart(2, "0"))
11
+ .replace(/\{PREFIX\}/g, "")
12
+ .replace(/\{RANDOM:(\d+)\}/g, (_, len) => {
13
+ const n = parseInt(len);
14
+ let s = "";
15
+ for (let i = 0; i < n; i++)
16
+ s += Math.floor(Math.random() * 10);
17
+ return s;
18
+ })
19
+ .replace(/\{ALPHA:(\d+)\}/g, (_, len) => {
20
+ const n = parseInt(len);
21
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
22
+ let s = "";
23
+ for (let i = 0; i < n; i++)
24
+ s += chars[Math.floor(Math.random() * 26)];
25
+ return s;
26
+ });
27
+ }
28
+ function normalizeGenerationRules(raw) {
29
+ if (!raw)
30
+ return [];
31
+ if (Array.isArray(raw))
32
+ return raw;
33
+ // Object-keyed format from DB: { "moisture": { "type": "random_range", "config": { "min": 8.5, ... } } }
34
+ // Convert to array format: [{ field: "moisture", type: "random_range", min: 8.5, ... }]
35
+ if (typeof raw === "object") {
36
+ const rules = [];
37
+ for (const [field, def] of Object.entries(raw)) {
38
+ if (!def || typeof def !== "object")
39
+ continue;
40
+ const cfg = def.config || {};
41
+ const rule = {
42
+ field,
43
+ type: def.type,
44
+ pattern: (cfg.pattern || cfg.prefix) ? `${cfg.prefix || ""}${cfg.pattern || ""}` : undefined,
45
+ choices: cfg.choices,
46
+ resolve_pattern: cfg.resolve_pattern,
47
+ min: cfg.min,
48
+ max: cfg.max,
49
+ decimals: cfg.decimals,
50
+ base_field: cfg.base,
51
+ value: cfg.value,
52
+ };
53
+ // relative_date: offset can be { days: [1, 2] } or { days: 5 }
54
+ if (cfg.offset) {
55
+ if (cfg.offset.days !== undefined) {
56
+ rule.offset = Array.isArray(cfg.offset.days) ? cfg.offset.days : cfg.offset.days;
57
+ }
58
+ else {
59
+ rule.offset = cfg.offset;
60
+ }
61
+ }
62
+ rules.push(rule);
63
+ }
64
+ return rules;
65
+ }
66
+ return [];
67
+ }
68
+ export function applyGenerationRules(rules, data) {
69
+ const normalized = normalizeGenerationRules(rules);
70
+ if (!normalized.length)
71
+ return data;
72
+ const result = { ...data };
73
+ // Multi-pass (max 4) to resolve chained dependencies (e.g. dateCollected → dateReceived → dateTested)
74
+ for (let pass = 0; pass < 4; pass++) {
75
+ let resolved = 0;
76
+ for (const rule of normalized) {
77
+ // User overrides win — skip fields already provided
78
+ if (result[rule.field] !== undefined && result[rule.field] !== null)
79
+ continue;
80
+ // For relative_date, skip if base field isn't resolved yet
81
+ if (rule.type === "relative_date" && rule.base_field) {
82
+ if (result[rule.base_field] === undefined || result[rule.base_field] === null)
83
+ continue;
84
+ }
85
+ switch (rule.type) {
86
+ case "pattern": {
87
+ result[rule.field] = resolvePattern(rule.pattern || "");
88
+ resolved++;
89
+ break;
90
+ }
91
+ case "random_choice": {
92
+ if (!rule.choices?.length)
93
+ break;
94
+ const pick = rule.choices[Math.floor(Math.random() * rule.choices.length)];
95
+ result[rule.field] = rule.resolve_pattern ? resolvePattern(pick) : pick;
96
+ resolved++;
97
+ break;
98
+ }
99
+ case "random_range": {
100
+ const min = rule.min ?? 0;
101
+ const max = rule.max ?? 100;
102
+ const raw = min + Math.random() * (max - min);
103
+ const dec = rule.decimals ?? 2;
104
+ result[rule.field] = parseFloat(raw.toFixed(dec));
105
+ resolved++;
106
+ break;
107
+ }
108
+ case "relative_date": {
109
+ const baseVal = result[rule.base_field || ""];
110
+ const base = baseVal ? new Date(baseVal) : new Date();
111
+ let offsetDays;
112
+ if (Array.isArray(rule.offset)) {
113
+ const [lo, hi] = rule.offset;
114
+ offsetDays = lo + Math.floor(Math.random() * (hi - lo + 1));
115
+ }
116
+ else {
117
+ offsetDays = rule.offset ?? 0;
118
+ }
119
+ const target = new Date(base);
120
+ target.setDate(target.getDate() + offsetDays);
121
+ result[rule.field] = target.toISOString().slice(0, 10);
122
+ resolved++;
123
+ break;
124
+ }
125
+ case "constant": {
126
+ const val = rule.value;
127
+ if (val === "TODAY") {
128
+ result[rule.field] = new Date().toISOString().slice(0, 10);
129
+ }
130
+ else {
131
+ result[rule.field] = val;
132
+ }
133
+ resolved++;
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ if (resolved === 0)
139
+ break; // No more fields to resolve
140
+ }
141
+ return result;
142
+ }
143
+ export function generateCannabinoidData(profileConfig, constants) {
144
+ if (!profileConfig)
145
+ return [];
146
+ const rows = [];
147
+ // Find LOD/LOQ constants — try common keys
148
+ const lodLoqKey = Object.keys(constants).find(k => k.startsWith("LOD_LOQ"));
149
+ const lodLoqMap = (lodLoqKey ? constants[lodLoqKey] : {});
150
+ const defaultLod = 0.01;
151
+ const defaultLoq = 0.03;
152
+ for (const [analyte, cfg] of Object.entries(profileConfig)) {
153
+ const lodLoq = lodLoqMap[analyte] || {};
154
+ const lod = lodLoq.LOD ?? defaultLod;
155
+ const loq = lodLoq.LOQ ?? defaultLoq;
156
+ if (cfg.nd) {
157
+ rows.push({ name: analyte, result: "ND", percentWeight: 0, mgPerG: 0, lod, loq });
158
+ }
159
+ else {
160
+ const min = cfg.min ?? 0;
161
+ const max = cfg.max ?? 100;
162
+ const percentWeight = parseFloat((min + Math.random() * (max - min)).toFixed(3));
163
+ const mgPerG = parseFloat((percentWeight * 10).toFixed(2));
164
+ rows.push({
165
+ name: analyte,
166
+ result: percentWeight < lod ? "ND" : percentWeight,
167
+ percentWeight,
168
+ mgPerG,
169
+ lod,
170
+ loq,
171
+ });
172
+ }
173
+ }
174
+ return rows;
175
+ }
176
+ // ============================================================================
177
+ // 2b. FULL PANEL DATA GENERATOR — safety tests from template constants
178
+ // ============================================================================
179
+ export function generateFullPanelData(constants) {
180
+ const microbialTests = (constants["MICROBIAL_TESTS"] || {});
181
+ const heavyMetals = (constants["HEAVY_METALS"] || {});
182
+ const mycotoxins = (constants["MYCOTOXINS"] || {});
183
+ const pesticidesCat1 = (constants["PESTICIDES_CATEGORY_I"] || []);
184
+ const pesticidesCat2 = (constants["PESTICIDES_CATEGORY_II"] || []);
185
+ const microbialResults = Object.entries(microbialTests).map(([test, config]) => {
186
+ let result = "ND";
187
+ if (!config.nd_required && config.typical_range) {
188
+ if (Math.random() > (config.nd_chance || 0)) {
189
+ result = Math.round(config.typical_range.min + Math.random() * (config.typical_range.max - config.typical_range.min));
190
+ }
191
+ }
192
+ return { test, result, limit: config.limit, status: "Pass" };
193
+ });
194
+ const heavyMetalsResults = Object.entries(heavyMetals).map(([analyte, config]) => {
195
+ let result = "ND";
196
+ if (Math.random() > config.nd_chance) {
197
+ const val = Math.round((config.typical_range.min + Math.random() * (config.typical_range.max - config.typical_range.min)) * 1000) / 1000;
198
+ result = val < config.loq ? "ND" : val;
199
+ }
200
+ return { analyte, lod: config.lod, loq: config.loq, result, limit: config.limit, status: "Pass" };
201
+ });
202
+ const mycotoxinResults = Object.entries(mycotoxins).map(([analyte, config]) => ({
203
+ analyte, lod: config.lod, loq: config.loq, result: "ND", limit: config.limit, status: "Pass",
204
+ }));
205
+ const pesticidesCat1Results = pesticidesCat1.map(analyte => ({ analyte, result: "ND", status: "Pass" }));
206
+ const pesticidesCat2Results = pesticidesCat2.map(analyte => ({ analyte, result: "ND", status: "Pass" }));
207
+ const residualSolventsData = [
208
+ { name: "Acetone", limit: 5000 }, { name: "Benzene", limit: 2 }, { name: "Butane", limit: 5000 },
209
+ { name: "Chloroform", limit: 2 }, { name: "Ethanol", limit: 5000 }, { name: "Ethyl Acetate", limit: 5000 },
210
+ { name: "Heptane", limit: 5000 }, { name: "Hexane", limit: 290 }, { name: "Isopropyl Alcohol", limit: 5000 },
211
+ { name: "Methanol", limit: 3000 }, { name: "Pentane", limit: 5000 }, { name: "Propane", limit: 5000 },
212
+ { name: "Toluene", limit: 890 }, { name: "Xylene", limit: 2170 },
213
+ ];
214
+ const residualSolventsResults = residualSolventsData.map(s => ({
215
+ analyte: s.name, result: "<LOQ", limit: s.limit, loq: s.limit * 0.1, unit: "ppm", status: "Pass",
216
+ }));
217
+ return { microbialResults, heavyMetalsResults, mycotoxinResults, pesticidesCat1: pesticidesCat1Results, pesticidesCat2: pesticidesCat2Results, residualSolventsResults };
218
+ }
219
+ function tokenize(expr) {
220
+ const tokens = [];
221
+ let i = 0;
222
+ while (i < expr.length) {
223
+ const ch = expr[i];
224
+ if (/\s/.test(ch)) {
225
+ i++;
226
+ continue;
227
+ }
228
+ if (/[0-9.]/.test(ch)) {
229
+ let num = "";
230
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
231
+ num += expr[i];
232
+ i++;
233
+ }
234
+ tokens.push({ type: "NUMBER", value: num });
235
+ continue;
236
+ }
237
+ if (/[a-zA-Z_]/.test(ch)) {
238
+ let id = "";
239
+ while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
240
+ id += expr[i];
241
+ i++;
242
+ }
243
+ if (id === "SUM" && i < expr.length && expr[i] === "(") {
244
+ tokens.push({ type: "FUNC", value: id });
245
+ }
246
+ else {
247
+ tokens.push({ type: "IDENT", value: id });
248
+ }
249
+ continue;
250
+ }
251
+ if ("+-*/".includes(ch)) {
252
+ tokens.push({ type: "OP", value: ch });
253
+ i++;
254
+ continue;
255
+ }
256
+ if (ch === "(") {
257
+ tokens.push({ type: "LPAREN", value: ch });
258
+ i++;
259
+ continue;
260
+ }
261
+ if (ch === ")") {
262
+ tokens.push({ type: "RPAREN", value: ch });
263
+ i++;
264
+ continue;
265
+ }
266
+ if (ch === "[") {
267
+ tokens.push({ type: "LBRACKET", value: ch });
268
+ i++;
269
+ continue;
270
+ }
271
+ if (ch === "]") {
272
+ tokens.push({ type: "RBRACKET", value: ch });
273
+ i++;
274
+ continue;
275
+ }
276
+ if (ch === ".") {
277
+ tokens.push({ type: "DOT", value: ch });
278
+ i++;
279
+ continue;
280
+ }
281
+ if (ch === ",") {
282
+ tokens.push({ type: "COMMA", value: ch });
283
+ i++;
284
+ continue;
285
+ }
286
+ i++; // skip unknown
287
+ }
288
+ tokens.push({ type: "EOF", value: "" });
289
+ return tokens;
290
+ }
291
+ // Recursive descent parser
292
+ class ExprParser {
293
+ tokens;
294
+ ctx;
295
+ pos = 0;
296
+ constructor(tokens, ctx) {
297
+ this.tokens = tokens;
298
+ this.ctx = ctx;
299
+ }
300
+ peek() { return this.tokens[this.pos] || { type: "EOF", value: "" }; }
301
+ advance() { return this.tokens[this.pos++]; }
302
+ parse() {
303
+ const val = this.expr();
304
+ return val;
305
+ }
306
+ expr() {
307
+ let left = this.term();
308
+ while (this.peek().type === "OP" && (this.peek().value === "+" || this.peek().value === "-")) {
309
+ const op = this.advance().value;
310
+ const right = this.term();
311
+ left = op === "+" ? left + right : left - right;
312
+ }
313
+ return left;
314
+ }
315
+ term() {
316
+ let left = this.unary();
317
+ while (this.peek().type === "OP" && (this.peek().value === "*" || this.peek().value === "/")) {
318
+ const op = this.advance().value;
319
+ const right = this.unary();
320
+ left = op === "*" ? left * right : (right !== 0 ? left / right : 0);
321
+ }
322
+ return left;
323
+ }
324
+ unary() {
325
+ if (this.peek().type === "OP" && this.peek().value === "-") {
326
+ this.advance();
327
+ return -this.primary();
328
+ }
329
+ return this.primary();
330
+ }
331
+ primary() {
332
+ const tok = this.peek();
333
+ if (tok.type === "NUMBER") {
334
+ this.advance();
335
+ return parseFloat(tok.value);
336
+ }
337
+ if (tok.type === "LPAREN") {
338
+ this.advance();
339
+ const val = this.expr();
340
+ if (this.peek().type === "RPAREN")
341
+ this.advance();
342
+ return val;
343
+ }
344
+ // SUM(array[].field)
345
+ if (tok.type === "FUNC" && tok.value === "SUM") {
346
+ this.advance(); // SUM
347
+ if (this.peek().type === "LPAREN")
348
+ this.advance(); // (
349
+ // Parse: arrayName[].fieldName
350
+ const arrayName = this.advance().value; // identifier
351
+ if (this.peek().type === "LBRACKET") {
352
+ this.advance(); // [
353
+ if (this.peek().type === "RBRACKET")
354
+ this.advance(); // ]
355
+ }
356
+ if (this.peek().type === "DOT")
357
+ this.advance(); // .
358
+ const fieldName = this.advance().value; // field
359
+ if (this.peek().type === "RPAREN")
360
+ this.advance(); // )
361
+ const arr = this.ctx[arrayName];
362
+ if (!Array.isArray(arr))
363
+ return 0;
364
+ let sum = 0;
365
+ for (const item of arr) {
366
+ const v = item[fieldName];
367
+ if (typeof v === "number")
368
+ sum += v;
369
+ }
370
+ return sum;
371
+ }
372
+ // Identifier — resolve from context
373
+ if (tok.type === "IDENT") {
374
+ this.advance();
375
+ const val = this.ctx[tok.value];
376
+ if (typeof val === "number")
377
+ return val;
378
+ if (typeof val === "string") {
379
+ const n = parseFloat(val);
380
+ return isNaN(n) ? 0 : n;
381
+ }
382
+ return 0;
383
+ }
384
+ // Fallback
385
+ this.advance();
386
+ return 0;
387
+ }
388
+ }
389
+ function safeEval(expr, ctx) {
390
+ const tokens = tokenize(expr);
391
+ const parser = new ExprParser(tokens, ctx);
392
+ return parser.parse();
393
+ }
394
+ export function applyCalculations(calculations, constants, data) {
395
+ if (!calculations || !Array.isArray(calculations))
396
+ return data;
397
+ const result = { ...data };
398
+ // Flatten constants into context
399
+ const ctx = { ...result };
400
+ for (const [k, v] of Object.entries(constants)) {
401
+ if (typeof v === "number" || typeof v === "string") {
402
+ ctx[k] = v;
403
+ }
404
+ }
405
+ // Multi-pass (max 3) for inter-formula dependencies
406
+ for (let pass = 0; pass < 3; pass++) {
407
+ let changed = false;
408
+ for (const calc of calculations) {
409
+ try {
410
+ const val = safeEval(calc.formula, ctx);
411
+ const rounded = parseFloat(val.toFixed(calc.decimals ?? 3));
412
+ if (ctx[calc.field] !== rounded) {
413
+ ctx[calc.field] = rounded;
414
+ result[calc.field] = rounded;
415
+ changed = true;
416
+ }
417
+ }
418
+ catch {
419
+ // Skip failed calculations
420
+ }
421
+ }
422
+ if (!changed)
423
+ break;
424
+ }
425
+ return result;
426
+ }
427
+ function tokenizeBool(expr) {
428
+ const tokens = [];
429
+ let i = 0;
430
+ while (i < expr.length) {
431
+ const ch = expr[i];
432
+ if (/\s/.test(ch)) {
433
+ i++;
434
+ continue;
435
+ }
436
+ if (/[0-9.]/.test(ch)) {
437
+ let num = "";
438
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
439
+ num += expr[i];
440
+ i++;
441
+ }
442
+ tokens.push({ type: "NUMBER", value: num });
443
+ continue;
444
+ }
445
+ if (/[a-zA-Z_]/.test(ch)) {
446
+ let id = "";
447
+ while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
448
+ id += expr[i];
449
+ i++;
450
+ }
451
+ if (id === "AND" || id === "OR") {
452
+ tokens.push({ type: "LOGIC", value: id });
453
+ }
454
+ else {
455
+ tokens.push({ type: "IDENT", value: id });
456
+ }
457
+ continue;
458
+ }
459
+ // Comparison operators
460
+ if (ch === "<" || ch === ">" || ch === "=" || ch === "!") {
461
+ let op = ch;
462
+ i++;
463
+ if (i < expr.length && expr[i] === "=") {
464
+ op += "=";
465
+ i++;
466
+ }
467
+ else if (ch === "=" && i < expr.length && expr[i] === "=") {
468
+ op += "=";
469
+ i++;
470
+ }
471
+ tokens.push({ type: "CMP", value: op });
472
+ continue;
473
+ }
474
+ if ("+-*/".includes(ch)) {
475
+ tokens.push({ type: "OP", value: ch });
476
+ i++;
477
+ continue;
478
+ }
479
+ if (ch === "(") {
480
+ tokens.push({ type: "LPAREN", value: ch });
481
+ i++;
482
+ continue;
483
+ }
484
+ if (ch === ")") {
485
+ tokens.push({ type: "RPAREN", value: ch });
486
+ i++;
487
+ continue;
488
+ }
489
+ i++;
490
+ }
491
+ tokens.push({ type: "EOF", value: "" });
492
+ return tokens;
493
+ }
494
+ function evalBoolExpr(expr, ctx) {
495
+ const tokens = tokenizeBool(expr);
496
+ let pos = 0;
497
+ function peek() { return tokens[pos] || { type: "EOF", value: "" }; }
498
+ function advance() { return tokens[pos++]; }
499
+ function numericValue() {
500
+ const tok = peek();
501
+ if (tok.type === "NUMBER") {
502
+ advance();
503
+ return parseFloat(tok.value);
504
+ }
505
+ if (tok.type === "IDENT") {
506
+ advance();
507
+ const v = ctx[tok.value];
508
+ return typeof v === "number" ? v : (typeof v === "string" ? parseFloat(v) || 0 : 0);
509
+ }
510
+ if (tok.type === "LPAREN") {
511
+ advance();
512
+ const val = numericExpr();
513
+ if (peek().type === "RPAREN")
514
+ advance();
515
+ return val;
516
+ }
517
+ advance();
518
+ return 0;
519
+ }
520
+ function numericExpr() {
521
+ let left = numericTerm();
522
+ while (peek().type === "OP" && (peek().value === "+" || peek().value === "-")) {
523
+ const op = advance().value;
524
+ const right = numericTerm();
525
+ left = op === "+" ? left + right : left - right;
526
+ }
527
+ return left;
528
+ }
529
+ function numericTerm() {
530
+ let left = numericValue();
531
+ while (peek().type === "OP" && (peek().value === "*" || peek().value === "/")) {
532
+ const op = advance().value;
533
+ const right = numericValue();
534
+ left = op === "*" ? left * right : (right !== 0 ? left / right : 0);
535
+ }
536
+ return left;
537
+ }
538
+ function comparison() {
539
+ const left = numericExpr();
540
+ const tok = peek();
541
+ if (tok.type === "CMP") {
542
+ const op = advance().value;
543
+ const right = numericExpr();
544
+ switch (op) {
545
+ case "<": return left < right;
546
+ case "<=": return left <= right;
547
+ case ">": return left > right;
548
+ case ">=": return left >= right;
549
+ case "==": return Math.abs(left - right) < 1e-9;
550
+ case "!=": return Math.abs(left - right) >= 1e-9;
551
+ default: return false;
552
+ }
553
+ }
554
+ // Truthy check if no comparison operator
555
+ return left !== 0;
556
+ }
557
+ function boolExpr() {
558
+ let left = comparison();
559
+ while (peek().type === "LOGIC") {
560
+ const op = advance().value;
561
+ const right = comparison();
562
+ if (op === "AND")
563
+ left = left && right;
564
+ else if (op === "OR")
565
+ left = left || right;
566
+ }
567
+ return left;
568
+ }
569
+ return boolExpr();
570
+ }
571
+ export function runValidation(rules, data) {
572
+ if (!rules || !Array.isArray(rules))
573
+ return [];
574
+ const results = [];
575
+ for (const rule of rules) {
576
+ try {
577
+ const passed = evalBoolExpr(rule.formula, data);
578
+ results.push({ name: rule.name, severity: rule.severity, message: rule.message, passed });
579
+ }
580
+ catch {
581
+ results.push({ name: rule.name, severity: rule.severity, message: rule.message, passed: false });
582
+ }
583
+ }
584
+ return results;
585
+ }
586
+ export function resolveBinding(bind, data) {
587
+ const parts = bind.split(".");
588
+ let val = data;
589
+ for (const p of parts) {
590
+ if (val === null || val === undefined)
591
+ return undefined;
592
+ val = val[p];
593
+ }
594
+ return val;
595
+ }
596
+ export function resolveText(text, data) {
597
+ if (!text)
598
+ return "";
599
+ return fillTemplate(text, data);
600
+ }
601
+ // Minimal QR code generator — produces SVG rect elements for a text payload.
602
+ // Uses a simple bit matrix approach sufficient for short strings (URLs, IDs).
603
+ export function generateQRSvg(text, size = 100) {
604
+ // Simple QR-like encoding: create a deterministic pattern from text hash
605
+ // This generates a visually QR-like pattern (not scannable — for display purposes).
606
+ // For real QR, the template data usually includes a pre-generated QR URL.
607
+ const moduleCount = 21; // QR Version 1
608
+ const cellSize = size / moduleCount;
609
+ const modules = Array.from({ length: moduleCount }, () => Array(moduleCount).fill(false));
610
+ // Finder patterns (top-left, top-right, bottom-left)
611
+ const drawFinder = (row, col) => {
612
+ for (let r = 0; r < 7; r++) {
613
+ for (let c = 0; c < 7; c++) {
614
+ const isEdge = r === 0 || r === 6 || c === 0 || c === 6;
615
+ const isInner = r >= 2 && r <= 4 && c >= 2 && c <= 4;
616
+ if (isEdge || isInner)
617
+ modules[row + r][col + c] = true;
618
+ }
619
+ }
620
+ };
621
+ drawFinder(0, 0);
622
+ drawFinder(0, moduleCount - 7);
623
+ drawFinder(moduleCount - 7, 0);
624
+ // Data area: hash the text and fill deterministically
625
+ let hash = 0;
626
+ for (let i = 0; i < text.length; i++) {
627
+ hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
628
+ }
629
+ for (let r = 8; r < moduleCount; r++) {
630
+ for (let c = 8; c < moduleCount; c++) {
631
+ hash = ((hash << 5) - hash + r * 31 + c * 17) | 0;
632
+ modules[r][c] = (hash & 1) === 1;
633
+ }
634
+ }
635
+ let rects = "";
636
+ for (let r = 0; r < moduleCount; r++) {
637
+ for (let c = 0; c < moduleCount; c++) {
638
+ if (modules[r][c]) {
639
+ rects += `<rect x="${c * cellSize}" y="${r * cellSize}" width="${cellSize}" height="${cellSize}" fill="#000"/>`;
640
+ }
641
+ }
642
+ }
643
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}"><rect width="${size}" height="${size}" fill="#fff"/>${rects}</svg>`;
644
+ }
645
+ function renderElement(el, data) {
646
+ switch (el.type) {
647
+ case "text": {
648
+ const text = resolveText(el.content || el.text || "", data);
649
+ const styles = [];
650
+ if (el.fontSize)
651
+ styles.push(`font-size:${el.fontSize}px`);
652
+ if (el.color)
653
+ styles.push(`color:${el.color}`);
654
+ if (el.bold)
655
+ styles.push("font-weight:bold");
656
+ if (el.align)
657
+ styles.push(`text-align:${el.align}`);
658
+ return `<div style="${styles.join(";")}">${text}</div>`;
659
+ }
660
+ case "header": {
661
+ const text = resolveText(el.content || el.text || "", data);
662
+ const styles = ["font-weight:bold"];
663
+ if (el.backgroundColor)
664
+ styles.push(`background-color:${el.backgroundColor}`);
665
+ if (el.padding)
666
+ styles.push(`padding:${typeof el.padding === "number" ? el.padding + "px" : el.padding}`);
667
+ if (el.fontSize)
668
+ styles.push(`font-size:${el.fontSize}px`);
669
+ if (el.color)
670
+ styles.push(`color:${el.color}`);
671
+ if (el.align)
672
+ styles.push(`text-align:${el.align}`);
673
+ return `<div style="${styles.join(";")}">${text}</div>`;
674
+ }
675
+ case "image": {
676
+ let src = el.src || el.url || "";
677
+ if (el.bind) {
678
+ const resolved = resolveBinding(el.bind, data);
679
+ if (typeof resolved === "string")
680
+ src = resolved;
681
+ }
682
+ const w = el.width ? `width:${typeof el.width === "number" ? el.width + "px" : el.width}` : "";
683
+ const h = el.height ? `height:${typeof el.height === "number" ? el.height + "px" : el.height}` : "";
684
+ return `<img src="${src}" style="${[w, h].filter(Boolean).join(";")}" />`;
685
+ }
686
+ case "qrcode": {
687
+ let qrData = el.data || el.content || "";
688
+ if (el.bind) {
689
+ const resolved = resolveBinding(el.bind, data);
690
+ if (typeof resolved === "string")
691
+ qrData = resolved;
692
+ }
693
+ qrData = resolveText(qrData, data);
694
+ const qrSize = el.size || (typeof el.width === "number" ? el.width : 100);
695
+ const svg = generateQRSvg(qrData, qrSize);
696
+ const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
697
+ return `<img src="${dataUri}" width="${qrSize}" height="${qrSize}" />`;
698
+ }
699
+ case "spacer": {
700
+ const h = el.height || 10;
701
+ return `<div style="height:${typeof h === "number" ? h + "px" : h}"></div>`;
702
+ }
703
+ case "line": {
704
+ const color = el.color || "#000";
705
+ const thickness = el.thickness || 1;
706
+ return `<hr style="border:none;border-top:${thickness}px solid ${color};margin:8px 0;" />`;
707
+ }
708
+ case "columns": {
709
+ const cols = el.columns || el.children || [];
710
+ const inner = cols.map((child) => {
711
+ // Support both: array of elements per column, or single element per column
712
+ if (Array.isArray(child)) {
713
+ return `<div style="flex:1">${child.map((c) => renderElement(c, data)).join("")}</div>`;
714
+ }
715
+ return `<div style="flex:1">${renderElement(child, data)}</div>`;
716
+ }).join("");
717
+ return `<div style="display:flex;gap:8px">${inner}</div>`;
718
+ }
719
+ case "page_break": {
720
+ return `<div style="page-break-after:always"></div>`;
721
+ }
722
+ case "grid": {
723
+ const items = (Array.isArray(el.items) ? el.items : Array.isArray(el.content) ? el.content : []);
724
+ const gridRows = items.map((item) => {
725
+ const label = item.label || item.value || "";
726
+ let value = "";
727
+ if (item.bind) {
728
+ const resolved = resolveBinding(item.bind, data);
729
+ value = resolved !== undefined && resolved !== null ? String(resolved) : "";
730
+ }
731
+ value = resolveText(value || "", data);
732
+ return `<tr><td style="padding:4px 8px;font-weight:bold;white-space:nowrap;width:40%">${label}</td><td style="padding:4px 8px">${value}</td></tr>`;
733
+ }).join("");
734
+ return `<table style="width:100%;border-collapse:collapse">${gridRows}</table>`;
735
+ }
736
+ case "table": {
737
+ const headers = el.headers || [];
738
+ const rowFields = el.row_fields;
739
+ const thead = headers.length
740
+ ? `<thead><tr>${headers.map(h => `<th style="padding:6px 8px;border:1px solid #ddd;background:#f5f5f5;text-align:left;font-size:8px">${h}</th>`).join("")}</tr></thead>`
741
+ : "";
742
+ let bodyRows = el.rows || [];
743
+ if (el.bind) {
744
+ const resolved = resolveBinding(el.bind, data);
745
+ if (Array.isArray(resolved))
746
+ bodyRows = resolved;
747
+ }
748
+ const tbody = bodyRows.map((row) => {
749
+ if (Array.isArray(row)) {
750
+ return `<tr>${row.map(cell => `<td style="padding:5px 8px;border:1px solid #ddd;font-size:8px">${cell ?? ""}</td>`).join("")}</tr>`;
751
+ }
752
+ // Object row — use row_fields if provided, else fall back to header matching
753
+ if (rowFields && rowFields.length) {
754
+ return `<tr>${rowFields.map(field => {
755
+ const val = row[field] ?? "";
756
+ return `<td style="padding:5px 8px;border:1px solid #ddd;font-size:8px">${val}</td>`;
757
+ }).join("")}</tr>`;
758
+ }
759
+ return `<tr>${headers.map(h => {
760
+ const key = h.toLowerCase().replace(/[^a-z0-9]/g, "");
761
+ const val = row[h] ?? row[key] ?? row[h.replace(/\s+/g, "")] ?? "";
762
+ return `<td style="padding:5px 8px;border:1px solid #ddd;font-size:8px">${val}</td>`;
763
+ }).join("")}</tr>`;
764
+ }).join("");
765
+ return `<table style="width:100%;border-collapse:collapse">${thead}<tbody>${tbody}</tbody></table>`;
766
+ }
767
+ case "box": {
768
+ const styles = ["text-align:center"];
769
+ if (el.border)
770
+ styles.push(`border:${el.border}`);
771
+ if (el.background || el.backgroundColor)
772
+ styles.push(`background:${el.background || el.backgroundColor}`);
773
+ if (el.padding)
774
+ styles.push(`padding:${typeof el.padding === "number" ? el.padding + "px" : el.padding}`);
775
+ const inner = (el.children || []).map(child => renderElement(child, data)).join("");
776
+ const text = el.content || el.text ? resolveText(el.content || el.text || "", data) : "";
777
+ return `<div style="${styles.join(";")}">${text}${inner}</div>`;
778
+ }
779
+ default: {
780
+ // Unknown element — try to render children
781
+ if (el.children && Array.isArray(el.children)) {
782
+ return el.children.map(child => renderElement(child, data)).join("");
783
+ }
784
+ return "";
785
+ }
786
+ }
787
+ }
788
+ export function renderLayoutToHtml(layout, pageConfig, styles, data) {
789
+ const pageSize = pageConfig?.size || "A4";
790
+ const margins = pageConfig?.margins || { top: "15mm", right: "15mm", bottom: "15mm", left: "15mm" };
791
+ const fontFamily = styles?.fontFamily || "Helvetica, Arial, sans-serif";
792
+ const fontSize = styles?.fontSize || 12;
793
+ const color = styles?.color || "#000";
794
+ // Normalize layout to LayoutElement[]:
795
+ // - Array → use directly
796
+ // - Object with .pages → flatten page sections (header/body/footer per page)
797
+ // - Object with .sections/.children → unwrap
798
+ // - Object with .header/.footer → wrap as elements
799
+ let elements = [];
800
+ if (Array.isArray(layout)) {
801
+ elements = layout;
802
+ }
803
+ else if (layout && typeof layout === "object") {
804
+ if (Array.isArray(layout.pages)) {
805
+ for (const page of layout.pages) {
806
+ if (page.header)
807
+ elements.push(page.header);
808
+ if (Array.isArray(page.body))
809
+ elements.push(...page.body);
810
+ else if (page.body)
811
+ elements.push(page.body);
812
+ if (Array.isArray(page.sections))
813
+ elements.push(...page.sections);
814
+ if (Array.isArray(page.elements))
815
+ elements.push(...page.elements);
816
+ if (page.footer)
817
+ elements.push(page.footer);
818
+ }
819
+ }
820
+ else if (Array.isArray(layout.sections)) {
821
+ elements = layout.sections;
822
+ }
823
+ else if (Array.isArray(layout.children)) {
824
+ elements = layout.children;
825
+ }
826
+ else {
827
+ // Object with header/footer keys — wrap as single-element layout
828
+ if (layout.header)
829
+ elements.push(layout.header);
830
+ if (layout.body) {
831
+ const body = layout.body;
832
+ if (Array.isArray(body))
833
+ elements.push(...body);
834
+ else
835
+ elements.push(body);
836
+ }
837
+ if (layout.footer)
838
+ elements.push(layout.footer);
839
+ }
840
+ }
841
+ const bodyHtml = elements.map(el => renderElement(el, data)).join("\n");
842
+ return `<!DOCTYPE html>
843
+ <html>
844
+ <head>
845
+ <meta charset="utf-8">
846
+ <style>
847
+ @page {
848
+ size: ${pageSize};
849
+ margin: ${margins.top || "15mm"} ${margins.right || "15mm"} ${margins.bottom || "15mm"} ${margins.left || "15mm"};
850
+ }
851
+ body {
852
+ font-family: ${fontFamily};
853
+ font-size: ${fontSize}px;
854
+ color: ${color};
855
+ margin: 0;
856
+ padding: 0;
857
+ line-height: 1.4;
858
+ }
859
+ table { page-break-inside: avoid; }
860
+ img { max-width: 100%; }
861
+ </style>
862
+ </head>
863
+ <body>
864
+ ${bodyHtml}
865
+ </body>
866
+ </html>`;
867
+ }