@hailer/mcp 1.1.17-beta.2 → 1.2.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.
- package/.claude/CLAUDE.md +94 -135
- package/.claude/skills/create-and-publish-app/SKILL.md +127 -0
- package/.claude/skills/hailer-app-builder/SKILL.md +47 -0
- package/.claude/skills/publish-hailer-app/SKILL.md +63 -4
- package/.claude/skills/sdk-function-fields/SKILL.md +431 -287
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +2 -0
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/bot.d.ts +2 -1
- package/dist/bot/bot.d.ts.map +1 -1
- package/dist/bot/bot.js +109 -41
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/services/message-classifier.d.ts.map +1 -1
- package/dist/bot/services/message-classifier.js +6 -0
- package/dist/bot/services/message-classifier.js.map +1 -1
- package/dist/bot/services/signal-router.d.ts.map +1 -1
- package/dist/bot/services/signal-router.js +1 -0
- package/dist/bot/services/signal-router.js.map +1 -1
- package/dist/bot/services/system-prompt.d.ts +4 -0
- package/dist/bot/services/system-prompt.d.ts.map +1 -1
- package/dist/bot/services/system-prompt.js +41 -12
- package/dist/bot/services/system-prompt.js.map +1 -1
- package/dist/bot/services/types.d.ts +7 -31
- package/dist/bot/services/types.d.ts.map +1 -1
- package/dist/bot/services/workspace-refresh.js.map +1 -1
- package/dist/bot/workspace-overview.d.ts.map +1 -1
- package/dist/bot/workspace-overview.js +4 -1
- package/dist/bot/workspace-overview.js.map +1 -1
- package/dist/bot-config/context.js.map +1 -1
- package/dist/bot-config/loader.d.ts.map +1 -1
- package/dist/bot-config/loader.js +1 -0
- package/dist/bot-config/loader.js.map +1 -1
- package/dist/bot-config/types.d.ts +2 -0
- package/dist/bot-config/types.d.ts.map +1 -1
- package/dist/mcp/UserContextCache.d.ts.map +1 -1
- package/dist/mcp/UserContextCache.js +8 -16
- package/dist/mcp/UserContextCache.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts +3 -2
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +14 -9
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/activity.d.ts.map +1 -1
- package/dist/mcp/tools/activity.js +39 -94
- package/dist/mcp/tools/activity.js.map +1 -1
- package/dist/mcp/tools/app-scaffold.d.ts.map +1 -1
- package/dist/mcp/tools/app-scaffold.js +300 -575
- package/dist/mcp/tools/app-scaffold.js.map +1 -1
- package/dist/mcp/tools/date.d.ts +5 -0
- package/dist/mcp/tools/date.d.ts.map +1 -0
- package/dist/mcp/tools/date.js +23 -0
- package/dist/mcp/tools/date.js.map +1 -0
- package/dist/mcp/tools/discussion.d.ts.map +1 -1
- package/dist/mcp/tools/discussion.js +17 -9
- package/dist/mcp/tools/discussion.js.map +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/insight.d.ts.map +1 -1
- package/dist/mcp/tools/insight.js +13 -19
- package/dist/mcp/tools/insight.js.map +1 -1
- package/dist/mcp/tools/workflow.d.ts +1 -0
- package/dist/mcp/tools/workflow.d.ts.map +1 -1
- package/dist/mcp/tools/workflow.js +293 -46
- package/dist/mcp/tools/workflow.js.map +1 -1
- package/dist/mcp/utils/data-transformers.d.ts +47 -10
- package/dist/mcp/utils/data-transformers.d.ts.map +1 -1
- package/dist/mcp/utils/data-transformers.js +12 -9
- package/dist/mcp/utils/data-transformers.js.map +1 -1
- package/dist/mcp/utils/types.d.ts +2 -0
- package/dist/mcp/utils/types.d.ts.map +1 -1
- package/dist/mcp/utils/types.js.map +1 -1
- package/dist/mcp/webhook-handler.d.ts.map +1 -1
- package/dist/mcp/webhook-handler.js +4 -1
- package/dist/mcp/webhook-handler.js.map +1 -1
- package/dist/mcp/workspace-cache.d.ts +8 -2
- package/dist/mcp/workspace-cache.d.ts.map +1 -1
- package/dist/mcp/workspace-cache.js +12 -8
- package/dist/mcp/workspace-cache.js.map +1 -1
- package/dist/plugins/vipunen/tools.d.ts +1 -0
- package/dist/plugins/vipunen/tools.d.ts.map +1 -1
- package/dist/plugins/vipunen/tools.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.cjs +0 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sdk-function-fields
|
|
3
3
|
description: Complete guide to creating calculated function fields in Hailer - workflow, variable types, patterns
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
triggers:
|
|
6
6
|
- function field
|
|
7
7
|
- calculated field
|
|
@@ -20,7 +20,7 @@ Complete guide for creating calculated fields that auto-update when dependencies
|
|
|
20
20
|
## Overview
|
|
21
21
|
|
|
22
22
|
Function fields compute values from other fields. Examples:
|
|
23
|
-
- Total = Quantity
|
|
23
|
+
- Total = Quantity x Unit Price
|
|
24
24
|
- Days Until Due = Due Date - Today
|
|
25
25
|
- Invoice Total = Sum of line item prices (filtered by phase)
|
|
26
26
|
|
|
@@ -35,15 +35,14 @@ npm run pull
|
|
|
35
35
|
|
|
36
36
|
### 2. Add Field Definition (fields.ts)
|
|
37
37
|
|
|
38
|
+
Add to the `fields` array in `workspace/[Workflow]_[id]/fields.ts`:
|
|
39
|
+
|
|
38
40
|
```typescript
|
|
39
|
-
//
|
|
41
|
+
// NEW field — omit _id, server assigns it after push
|
|
40
42
|
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
key: "total_cost",
|
|
44
|
-
function: "@function:totalCost",
|
|
43
|
+
data: [],
|
|
44
|
+
function: "@function:total_cost_XXX", // Must match exported function name
|
|
45
45
|
functionEnabled: true,
|
|
46
|
-
editable: false,
|
|
47
46
|
functionVariables: {
|
|
48
47
|
quantity: {
|
|
49
48
|
type: "=",
|
|
@@ -53,46 +52,89 @@ npm run pull
|
|
|
53
52
|
type: "=",
|
|
54
53
|
data: [FieldIds.unit_price]
|
|
55
54
|
}
|
|
56
|
-
}
|
|
55
|
+
},
|
|
56
|
+
inviteToDiscussionOnChange: false,
|
|
57
|
+
key: "totalCost",
|
|
58
|
+
label: "Total Cost",
|
|
59
|
+
required: false,
|
|
60
|
+
type: "numeric"
|
|
57
61
|
}
|
|
58
62
|
```
|
|
59
63
|
|
|
64
|
+
**Required properties for function fields:**
|
|
65
|
+
- `function`: `"@function:{functionName}"` — must match the exported function name exactly
|
|
66
|
+
- `functionEnabled`: `true`
|
|
67
|
+
- `functionVariables`: dependency wiring (see Variable Types below)
|
|
68
|
+
- `data`: `[]` — always empty array for function fields
|
|
69
|
+
- `type`: the output type (`"numeric"`, `"text"`, `"date"`, etc.)
|
|
70
|
+
|
|
71
|
+
**For new fields:** Omit `_id`. The server assigns it after push. Run `npm run pull` after push to get the server-assigned ID.
|
|
72
|
+
|
|
73
|
+
**For existing fields:** Keep the `_id` from enums.
|
|
74
|
+
|
|
60
75
|
### 3. Create Function File
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
**Naming convention:** `{functionName}_{last3HexOfFieldId}.ts`
|
|
78
|
+
|
|
79
|
+
- For new fields (no ID yet): use a temporary 3-char suffix, e.g., `total_cost_tmp.ts`
|
|
80
|
+
- After push + pull, the SDK renames to match the server-assigned ID
|
|
81
|
+
|
|
82
|
+
Create `workspace/[Workflow]_[id]/functions/total_cost_XXX.ts`:
|
|
63
83
|
|
|
64
84
|
```typescript
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Field function for: Total Cost
|
|
87
|
+
* Field name: Total Cost
|
|
88
|
+
* Field ID: [assigned after push]
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
interface Dependencies {
|
|
92
|
+
quantity: number;
|
|
93
|
+
unitPrice: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function total_cost_XXX(dep: Dependencies): number {
|
|
69
97
|
const qty = Number(dep.quantity) || 0;
|
|
70
98
|
const price = Number(dep.unitPrice) || 0;
|
|
71
99
|
return qty * price;
|
|
72
100
|
}
|
|
73
101
|
```
|
|
74
102
|
|
|
75
|
-
|
|
103
|
+
**File structure rules:**
|
|
104
|
+
1. **JSDoc header** — field function name, label, and ID
|
|
105
|
+
2. **`Dependencies` interface** — typed to match source field types (see Field Data Formats table)
|
|
106
|
+
3. **Named export** — function name must match the `@function:` reference in fields.ts
|
|
107
|
+
4. **Return type** — always explicit (`number`, `string`, etc.), never `any`
|
|
108
|
+
|
|
109
|
+
### 4. Export from Index
|
|
76
110
|
|
|
77
111
|
Edit `workspace/[Workflow]_[id]/functions/index.ts`:
|
|
78
112
|
|
|
79
113
|
```typescript
|
|
80
|
-
|
|
114
|
+
// Auto-generated index of field functions
|
|
115
|
+
// Import and re-export all field functions
|
|
116
|
+
|
|
117
|
+
export { total_cost_XXX } from "./total_cost_XXX";
|
|
81
118
|
```
|
|
82
119
|
|
|
83
|
-
### 5. Test
|
|
120
|
+
### 5. Test with Vitest
|
|
84
121
|
|
|
85
122
|
```typescript
|
|
86
|
-
// workspace/[Workflow]_[id]/
|
|
87
|
-
import {
|
|
123
|
+
// workspace/[Workflow]_[id]/functions/total_cost_XXX.test.ts
|
|
124
|
+
import { total_cost_XXX } from "./total_cost_XXX";
|
|
88
125
|
|
|
89
|
-
describe('
|
|
126
|
+
describe('total_cost_XXX', () => {
|
|
90
127
|
it('multiplies quantity by unit price', () => {
|
|
91
|
-
expect(
|
|
128
|
+
expect(total_cost_XXX({ quantity: 5, unitPrice: 10 })).toBe(50);
|
|
92
129
|
});
|
|
93
130
|
|
|
94
|
-
it('handles null values', () => {
|
|
95
|
-
expect(
|
|
131
|
+
it('handles null/undefined values', () => {
|
|
132
|
+
expect(total_cost_XXX({ quantity: null, unitPrice: 10 } as any)).toBe(0);
|
|
133
|
+
expect(total_cost_XXX({ quantity: 5, unitPrice: null } as any)).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles non-numeric values', () => {
|
|
137
|
+
expect(total_cost_XXX({ quantity: "abc", unitPrice: 10 } as any)).toBe(0);
|
|
96
138
|
});
|
|
97
139
|
});
|
|
98
140
|
```
|
|
@@ -105,278 +147,325 @@ Run: `npm test`
|
|
|
105
147
|
npm run fields-push:force
|
|
106
148
|
```
|
|
107
149
|
|
|
108
|
-
|
|
150
|
+
**Always use `:force`** — the non-force variant uses `fast-deep-equal` comparison which may skip `functionVariables` changes, so your dependency wiring won't update.
|
|
151
|
+
|
|
152
|
+
### 7. Pull to Get Server IDs
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run pull
|
|
156
|
+
```
|
|
109
157
|
|
|
110
|
-
|
|
158
|
+
After pull:
|
|
159
|
+
- Field gets a server-assigned `_id`
|
|
160
|
+
- Function file may be renamed to match the new ID suffix
|
|
161
|
+
- **Check enum imports** — identical hex suffixes across workflows can resolve to the wrong enum
|
|
111
162
|
|
|
112
|
-
|
|
163
|
+
**Important: Pull overwrites TypeScript declarations.** The server stores only the function body as plain JavaScript. On pull, the SDK regenerates the TypeScript wrapper (interface, return type, comments) from server metadata. Your type fixes get overwritten every time.
|
|
164
|
+
|
|
165
|
+
| Part of File | Persists After Pull? | Why |
|
|
166
|
+
|--------------|---------------------|-----|
|
|
167
|
+
| Function body code | Yes | Stored on server as-is |
|
|
168
|
+
| `Number()` wrapping | Yes | Part of function body |
|
|
169
|
+
| Named constants | Yes | Part of function body |
|
|
170
|
+
| Removed dependencies | Yes | Stored in server metadata |
|
|
171
|
+
| `interface Dependencies` types | No | SDK regenerates from schema |
|
|
172
|
+
| Return type annotation | No | SDK always uses `: any` |
|
|
173
|
+
| Comments/JSDoc | No | Not stored on server |
|
|
174
|
+
|
|
175
|
+
The SDK's type inference is imperfect — it may map `numeric` fields to `string` in the interface. This is cosmetic, not a runtime bug. **Always use defensive coding in the function body** (`Number()` wrapping, `|| 0` defaults) — never rely on TypeScript types for runtime safety. The function body is what runs on the server.
|
|
113
176
|
|
|
114
177
|
---
|
|
115
178
|
|
|
116
|
-
##
|
|
179
|
+
## Quality Rules
|
|
117
180
|
|
|
118
|
-
|
|
181
|
+
These rules prevent the bugs found in real function fields. Follow them exactly.
|
|
119
182
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
183
|
+
### Dependencies Interface Must Match Source Types
|
|
184
|
+
|
|
185
|
+
Map each `functionVariables` entry to its correct TypeScript type based on how it's wired:
|
|
186
|
+
|
|
187
|
+
| Variable type | Source | Dependencies type |
|
|
188
|
+
|---------------|--------|-------------------|
|
|
189
|
+
| `=` (same activity field) | numeric field | `number` |
|
|
190
|
+
| `=` (same activity field) | text field | `string` |
|
|
191
|
+
| `=` (same activity field) | date field | `number` (ms timestamp) |
|
|
192
|
+
| `<` (backlink to field) | any field | `Array<SourceType>` |
|
|
193
|
+
| `<` (backlink to `"meta"`) | metadata | `Array<ActivityMeta>` |
|
|
194
|
+
| `?` (static phase ID) | phase enum | `string` |
|
|
195
|
+
| `>` (forward link to field) | any field | `SourceType` |
|
|
196
|
+
| `>` (forward link to `"meta"`) | metadata | `ActivityMeta` |
|
|
197
|
+
| `=` with `data: ["meta"]` | own metadata | `ActivityMeta` |
|
|
198
|
+
|
|
199
|
+
**Wrong:** Typing a numeric source field as `string` (causes silent JS coercion)
|
|
200
|
+
**Right:** Always use the actual source field's type
|
|
201
|
+
|
|
202
|
+
### No Unused Dependencies
|
|
133
203
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Time field (ms timestamp - includes date!)
|
|
149
|
-
const startTime = dep.startTime; // 1765863000000
|
|
150
|
-
// To extract just the time, use Date methods:
|
|
151
|
-
const date = new Date(startTime);
|
|
152
|
-
const hours = date.getUTCHours();
|
|
153
|
-
const minutes = date.getUTCMinutes();
|
|
154
|
-
|
|
155
|
-
// Timerange field (object with start/end as ms timestamps - includes date!)
|
|
156
|
-
const workHours = dep.workingHours; // { start: 1765863000000, end: 1765915200000 }
|
|
157
|
-
const durationMs = workHours ? (workHours.end - workHours.start) : 0;
|
|
158
|
-
const durationMinutes = durationMs / 60000;
|
|
159
|
-
|
|
160
|
-
// IMPORTANT: When to convert vs keep raw:
|
|
161
|
-
// - Display only (text output) → use Date methods to extract time
|
|
162
|
-
// - Write to another date/time field → keep as Unix milliseconds (no conversion!)
|
|
163
|
-
|
|
164
|
-
// Checkbox field (1 or 0)
|
|
165
|
-
const isActive = dep.isActive; // 1 or 0
|
|
166
|
-
if (isActive === 1) { /* checked */ }
|
|
204
|
+
Every entry in `functionVariables` must be used in the function body. If you declare a dependency you don't use, it causes unnecessary cross-workflow lookups on every recalculation.
|
|
205
|
+
|
|
206
|
+
**Wrong:**
|
|
207
|
+
```typescript
|
|
208
|
+
interface Dependencies {
|
|
209
|
+
goals: Array<number>;
|
|
210
|
+
assists: Array<number>;
|
|
211
|
+
salary: Array<number>; // UNUSED — wasted cross-workflow lookup
|
|
212
|
+
}
|
|
213
|
+
export function rating(dep: Dependencies): number {
|
|
214
|
+
// salary is never referenced
|
|
215
|
+
return sumArray(dep.goals) * 2 + sumArray(dep.assists);
|
|
216
|
+
}
|
|
167
217
|
```
|
|
168
218
|
|
|
219
|
+
**Right:** Remove `salary` from both `functionVariables` in fields.ts AND the Dependencies interface.
|
|
220
|
+
|
|
221
|
+
### Return Type — Always Explicit, Never `any`
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// Wrong
|
|
225
|
+
export function myFunc(dep: Dependencies): any { ... }
|
|
226
|
+
|
|
227
|
+
// Right
|
|
228
|
+
export function myFunc(dep: Dependencies): number { ... }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Defensive Value Handling
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Scalars — default to zero/empty
|
|
235
|
+
const value = Number(dep.quantity) || 0;
|
|
236
|
+
const text = dep.name || "";
|
|
237
|
+
|
|
238
|
+
// Arrays (backlinks) — ALWAYS default to empty array
|
|
239
|
+
const items = dep.items || [];
|
|
240
|
+
|
|
241
|
+
// Metadata — null-check before property access
|
|
242
|
+
const phase = meta ? meta.phase : null;
|
|
243
|
+
|
|
244
|
+
// Array iteration — use for loop with index, null-check each element
|
|
245
|
+
for (let i = 0; i < items.length; i++) {
|
|
246
|
+
const val = Number(items[i]) || 0;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Runtime Is Plain JavaScript
|
|
251
|
+
|
|
252
|
+
Files are `.ts` for local dev and testing, but **the runtime strips TypeScript and executes plain ES6 JavaScript** in isolated-vm. This means:
|
|
253
|
+
- `interface` declarations are stripped — they're for dev-time safety only
|
|
254
|
+
- No TypeScript-only syntax in the function body (no `as`, no generics, no `!` assertions)
|
|
255
|
+
- Optional chaining (`?.`) and nullish coalescing (`??`) work (ES2020)
|
|
256
|
+
- `const`/`let`/`for...of`/arrow functions/template literals all work
|
|
257
|
+
|
|
169
258
|
---
|
|
170
259
|
|
|
171
|
-
##
|
|
260
|
+
## Variable Types Reference
|
|
172
261
|
|
|
173
|
-
|
|
262
|
+
### `=` — Same Activity Field
|
|
174
263
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
264
|
+
Read a field value from the same activity.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// fields.ts
|
|
268
|
+
functionVariables: {
|
|
269
|
+
quantity: {
|
|
270
|
+
type: "=",
|
|
271
|
+
data: [FieldIds.quantity]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// function — returns scalar value
|
|
276
|
+
const qty = Number(dep.quantity) || 0;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### `<` — Backlink (Other Activities Linking TO This One)
|
|
280
|
+
|
|
281
|
+
Returns **arrays** — multiple activities can link to this one.
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// fields.ts
|
|
285
|
+
functionVariables: {
|
|
178
286
|
prices: {
|
|
179
287
|
type: "<",
|
|
180
288
|
data: [WorkflowIds.invoice_rows, InvoiceRows_FieldIds.total_price]
|
|
181
289
|
}
|
|
182
290
|
}
|
|
183
291
|
|
|
184
|
-
//
|
|
292
|
+
// function — ALWAYS default to empty array
|
|
185
293
|
const prices = dep.prices || [];
|
|
186
294
|
let total = 0;
|
|
187
295
|
for (let i = 0; i < prices.length; i++) {
|
|
188
296
|
total += Number(prices[i]) || 0;
|
|
189
297
|
}
|
|
190
|
-
return total;
|
|
191
298
|
```
|
|
192
299
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
## Activity Metadata (`"meta"`)
|
|
196
|
-
|
|
197
|
-
The special `"meta"` value returns activity metadata. It works with **all variable types**:
|
|
300
|
+
### `>` — Forward Link (This Activity Links TO Another)
|
|
198
301
|
|
|
199
|
-
|
|
302
|
+
Read a field value from a linked activity.
|
|
200
303
|
|
|
201
|
-
```
|
|
202
|
-
//
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
type: "
|
|
206
|
-
data: [
|
|
304
|
+
```typescript
|
|
305
|
+
// fields.ts
|
|
306
|
+
functionVariables: {
|
|
307
|
+
customerName: {
|
|
308
|
+
type: ">",
|
|
309
|
+
data: [FieldIds.customer_link, Customers_FieldIds.name]
|
|
207
310
|
}
|
|
208
311
|
}
|
|
209
312
|
|
|
210
|
-
//
|
|
211
|
-
const
|
|
212
|
-
const myCreated = dep.myMeta ? dep.myMeta.created : 0;
|
|
313
|
+
// function — returns scalar value (single linked activity)
|
|
314
|
+
const name = dep.customerName || "";
|
|
213
315
|
```
|
|
214
316
|
|
|
215
|
-
###
|
|
317
|
+
### `?` — Static Value (Phase ID for Comparison)
|
|
216
318
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
319
|
+
Provides a known phase ID to compare against in logic.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// fields.ts
|
|
323
|
+
functionVariables: {
|
|
324
|
+
activePhase: {
|
|
325
|
+
type: "?",
|
|
326
|
+
data: [WorkflowIds.injuries, Injuries_PhaseIds.active]
|
|
223
327
|
}
|
|
224
328
|
}
|
|
225
329
|
|
|
226
|
-
//
|
|
227
|
-
const
|
|
330
|
+
// function — returns phase ID string
|
|
331
|
+
const activePhase = dep.activePhase; // "627d14339381d6077ab90ce9"
|
|
332
|
+
if (meta.phase === activePhase) { ... }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Activity Metadata (`"meta"`)
|
|
338
|
+
|
|
339
|
+
The special `"meta"` value returns activity metadata. Works with all variable types.
|
|
340
|
+
|
|
341
|
+
### Own Metadata (`=`)
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
functionVariables: {
|
|
345
|
+
myMeta: { type: "=", data: ["meta"] }
|
|
346
|
+
}
|
|
347
|
+
// dep.myMeta → { _id, name, phase, phaseName, created, updated, ... }
|
|
228
348
|
```
|
|
229
349
|
|
|
230
350
|
### Backlink Metadata (`<`)
|
|
231
351
|
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
{
|
|
235
|
-
rowsMeta: {
|
|
236
|
-
type: "<",
|
|
237
|
-
data: [WorkflowIds.invoice_rows, "meta"]
|
|
238
|
-
}
|
|
352
|
+
```typescript
|
|
353
|
+
functionVariables: {
|
|
354
|
+
injuryMeta: { type: "<", data: [WorkflowIds.injuries, "meta"] }
|
|
239
355
|
}
|
|
356
|
+
// dep.injuryMeta → Array<ActivityMeta>
|
|
357
|
+
```
|
|
240
358
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
359
|
+
### Forward Link Metadata (`>`)
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
functionVariables: {
|
|
363
|
+
customerMeta: { type: ">", data: [FieldIds.customer_link, "meta"] }
|
|
245
364
|
}
|
|
365
|
+
// dep.customerMeta → ActivityMeta
|
|
246
366
|
```
|
|
247
367
|
|
|
248
368
|
### Metadata Structure
|
|
249
369
|
|
|
250
|
-
```
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
370
|
+
```typescript
|
|
371
|
+
interface ActivityMeta {
|
|
372
|
+
_id: string; // Activity ID
|
|
373
|
+
name: string; // Activity name
|
|
374
|
+
process: string; // Workflow ID
|
|
375
|
+
phase: string; // Phase ID
|
|
376
|
+
processName: string; // Workflow name
|
|
377
|
+
phaseName: string; // Phase name
|
|
378
|
+
created: number; // Created timestamp (ms)
|
|
379
|
+
updated: number; // Updated timestamp (ms)
|
|
380
|
+
completed: number | null; // Completed timestamp or null
|
|
381
|
+
sequence: number; // Activity sequence number
|
|
382
|
+
active: boolean; // Is activity active
|
|
263
383
|
}
|
|
264
384
|
```
|
|
265
385
|
|
|
386
|
+
**Note:** `"data"` and `"meta"` are interchangeable aliases. Prefer `"meta"` for clarity.
|
|
387
|
+
|
|
266
388
|
---
|
|
267
389
|
|
|
268
390
|
## Parallel Array Guarantee
|
|
269
391
|
|
|
270
|
-
**CRITICAL
|
|
392
|
+
**CRITICAL:** When you have multiple `<` (backlink) variables from the **SAME source workflow**, they are guaranteed to be:
|
|
271
393
|
- Same length
|
|
272
394
|
- Same order (index 0 in all arrays = same activity)
|
|
273
395
|
|
|
274
|
-
|
|
396
|
+
This enables safe parallel iteration:
|
|
275
397
|
|
|
276
|
-
```
|
|
277
|
-
const prices = dep
|
|
278
|
-
const
|
|
398
|
+
```typescript
|
|
399
|
+
const prices = dep.prices || []; // [41.76, 26.1, 153.47]
|
|
400
|
+
const rowMeta = dep.rowMeta || []; // [{...}, {...}, {...}]
|
|
279
401
|
|
|
280
|
-
// Index 0 in both = same
|
|
402
|
+
// Index 0 in both = same source activity
|
|
281
403
|
for (let i = 0; i < prices.length; i++) {
|
|
282
404
|
const price = Number(prices[i]) || 0;
|
|
283
|
-
const phaseName =
|
|
284
|
-
// price and phaseName are from the SAME activity
|
|
405
|
+
const phaseName = rowMeta[i] ? rowMeta[i].phaseName : 'Unknown';
|
|
285
406
|
}
|
|
286
407
|
```
|
|
287
408
|
|
|
288
|
-
**Arrays from DIFFERENT workflows
|
|
409
|
+
**Arrays from DIFFERENT source workflows are independent — never assume index alignment across workflows.**
|
|
289
410
|
|
|
290
411
|
---
|
|
291
412
|
|
|
292
|
-
##
|
|
293
|
-
|
|
294
|
-
Static variables provide phase IDs for comparison in your function logic.
|
|
295
|
-
|
|
296
|
-
```javascript
|
|
297
|
-
// Get specific phase IDs to compare against
|
|
298
|
-
{
|
|
299
|
-
donePhase: {
|
|
300
|
-
type: "?",
|
|
301
|
-
data: [WorkflowIds.sales_activities, PhaseIds.done]
|
|
302
|
-
},
|
|
303
|
-
archivePhase: {
|
|
304
|
-
type: "?",
|
|
305
|
-
data: [WorkflowIds.sales_activities, PhaseIds.archive]
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// In function - compare against metadata phase
|
|
310
|
-
const donePhase = dep.donePhase; // "627d14339381d6077ab90ce9"
|
|
311
|
-
const archivePhase = dep.archivePhase; // "639b49617a4413c20f15ac12"
|
|
413
|
+
## Field Data Formats in Functions
|
|
312
414
|
|
|
313
|
-
|
|
314
|
-
// Activity is in done or archive phase
|
|
315
|
-
}
|
|
316
|
-
```
|
|
415
|
+
What format does each field type return when used as a dependency?
|
|
317
416
|
|
|
318
|
-
|
|
417
|
+
| Field Type | Returns | Example |
|
|
418
|
+
|------------|---------|---------|
|
|
419
|
+
| `text`, `textarea` | `string` | `"Hello world"` |
|
|
420
|
+
| `numeric`, `numericunit` | `number` | `42.5` |
|
|
421
|
+
| `date`, `datetime` | `number` (ms timestamp) | `1730937600000` |
|
|
422
|
+
| `daterange`, `datetimerange` | `{ start: number, end: number }` | `{ start: 1730937600000, end: 1731024000000 }` |
|
|
423
|
+
| `time` | `number` (ms timestamp, includes date!) | `1765863000000` |
|
|
424
|
+
| `timerange` | `{ start: number, end: number }` | `{ start: 1765863000000, end: 1765915200000 }` |
|
|
425
|
+
| `textpredefinedoptions` | `string` | `"High"` |
|
|
426
|
+
| `users`, `teams` | `string` (ID) | `"5f8a1b2c3d4e5f6a7b8c9d0e"` |
|
|
427
|
+
| `activitylink` | `string` (activity ID) | `"692abc123def456"` |
|
|
428
|
+
| `country` | `string` (ISO code) | `"FI"` |
|
|
429
|
+
| `numeric` + `modifier.checkbox` | `number` | `1` (true) or `0` (false) |
|
|
319
430
|
|
|
320
431
|
---
|
|
321
432
|
|
|
322
433
|
## Common Patterns
|
|
323
434
|
|
|
324
|
-
### Sum
|
|
435
|
+
### Sum Backlink Values
|
|
325
436
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Data: {
|
|
330
|
-
type: "<",
|
|
331
|
-
data: [WorkflowIds.sales_activities, "meta"]
|
|
332
|
-
},
|
|
333
|
-
dl: {
|
|
334
|
-
type: "<",
|
|
335
|
-
data: [WorkflowIds.sales_activities, FieldIds.deadline]
|
|
336
|
-
},
|
|
337
|
-
done: {
|
|
338
|
-
type: "?",
|
|
339
|
-
data: [WorkflowIds.sales_activities, PhaseIds.done]
|
|
340
|
-
},
|
|
341
|
-
archive: {
|
|
342
|
-
type: "?",
|
|
343
|
-
data: [WorkflowIds.sales_activities, PhaseIds.archive]
|
|
344
|
-
}
|
|
437
|
+
```typescript
|
|
438
|
+
interface Dependencies {
|
|
439
|
+
goals: Array<number>;
|
|
345
440
|
}
|
|
346
|
-
```
|
|
347
441
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const donePhase = dep.done;
|
|
354
|
-
const archivePhase = dep.archive;
|
|
355
|
-
|
|
356
|
-
const arr = [];
|
|
357
|
-
for (let i = 0; i < data.length; i++) {
|
|
358
|
-
const item = data[i];
|
|
359
|
-
if ((item.phase === donePhase || item.phase === archivePhase) && deadlines[i]) {
|
|
360
|
-
arr.push(deadlines[i]);
|
|
361
|
-
}
|
|
442
|
+
export function total_goals_XXX(dep: Dependencies): number {
|
|
443
|
+
const goals = dep.goals || [];
|
|
444
|
+
let sum = 0;
|
|
445
|
+
for (let i = 0; i < goals.length; i++) {
|
|
446
|
+
sum += Number(goals[i]) || 0;
|
|
362
447
|
}
|
|
363
|
-
|
|
364
|
-
if (arr.length === 0) return null;
|
|
365
|
-
return Math.max(...arr);
|
|
448
|
+
return sum;
|
|
366
449
|
}
|
|
367
450
|
```
|
|
368
451
|
|
|
369
|
-
### Sum
|
|
452
|
+
### Sum Backlinks Filtered by Phase
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
interface Dependencies {
|
|
456
|
+
prices: Array<number>;
|
|
457
|
+
rowMeta: Array<ActivityMeta>;
|
|
458
|
+
activePhase: string;
|
|
459
|
+
}
|
|
370
460
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const activePhase = dep['Active'];
|
|
461
|
+
export function sum_active_XXX(dep: Dependencies): number {
|
|
462
|
+
const prices = dep.prices || [];
|
|
463
|
+
const rowMeta = dep.rowMeta || [];
|
|
464
|
+
const activePhase = dep.activePhase;
|
|
376
465
|
|
|
377
466
|
let total = 0;
|
|
378
467
|
for (let i = 0; i < prices.length; i++) {
|
|
379
|
-
if (
|
|
468
|
+
if (rowMeta[i] && rowMeta[i].phase === activePhase) {
|
|
380
469
|
total += Number(prices[i]) || 0;
|
|
381
470
|
}
|
|
382
471
|
}
|
|
@@ -384,127 +473,182 @@ function sumActiveOnly(dep) {
|
|
|
384
473
|
}
|
|
385
474
|
```
|
|
386
475
|
|
|
387
|
-
###
|
|
476
|
+
### Composite Rating from Multiple Workflows
|
|
388
477
|
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
478
|
+
```typescript
|
|
479
|
+
interface Dependencies {
|
|
480
|
+
activePhase: string;
|
|
481
|
+
goals: Array<number>;
|
|
482
|
+
assists: Array<number>;
|
|
483
|
+
injuryMeta: Array<ActivityMeta>;
|
|
484
|
+
}
|
|
393
485
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
486
|
+
export function player_rating_XXX(dep: Dependencies): number {
|
|
487
|
+
const GOAL_WEIGHT = 2;
|
|
488
|
+
const INJURY_PENALTY = 5;
|
|
489
|
+
|
|
490
|
+
const goals = dep.goals || [];
|
|
491
|
+
const assists = dep.assists || [];
|
|
492
|
+
const injuryMeta = dep.injuryMeta || [];
|
|
493
|
+
const activePhase = dep.activePhase || null;
|
|
494
|
+
|
|
495
|
+
let totalGoals = 0;
|
|
496
|
+
for (let i = 0; i < goals.length; i++) {
|
|
497
|
+
totalGoals += Number(goals[i]) || 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let totalAssists = 0;
|
|
501
|
+
for (let i = 0; i < assists.length; i++) {
|
|
502
|
+
totalAssists += Number(assists[i]) || 0;
|
|
399
503
|
}
|
|
400
|
-
|
|
504
|
+
|
|
505
|
+
let activeInjuryCount = 0;
|
|
506
|
+
for (let i = 0; i < injuryMeta.length; i++) {
|
|
507
|
+
const meta = injuryMeta[i];
|
|
508
|
+
if (meta && activePhase && meta.phase === activePhase) {
|
|
509
|
+
activeInjuryCount++;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return (totalGoals * GOAL_WEIGHT + totalAssists) - (activeInjuryCount * INJURY_PENALTY);
|
|
401
514
|
}
|
|
402
515
|
```
|
|
403
516
|
|
|
404
|
-
###
|
|
517
|
+
### Per-90-Minutes Normalization
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
interface Dependencies {
|
|
521
|
+
goals: number;
|
|
522
|
+
minutesPlayed: number;
|
|
523
|
+
}
|
|
405
524
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const data = dep['Data'] || [];
|
|
410
|
-
const cancelledPhase = dep['Cancelled'];
|
|
411
|
-
const deletedPhase = dep['Deleted'];
|
|
525
|
+
export function goals_per_90_XXX(dep: Dependencies): number {
|
|
526
|
+
const goals = Number(dep.goals) || 0;
|
|
527
|
+
const minutes = Number(dep.minutesPlayed) || 0;
|
|
412
528
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
529
|
+
if (minutes === 0) return 0;
|
|
530
|
+
return Math.round((goals / minutes) * 90 * 100) / 100;
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Same-Activity Field Sum
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
interface Dependencies {
|
|
538
|
+
yellowCards: number;
|
|
539
|
+
redCards: number;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function total_cards_XXX(dep: Dependencies): number {
|
|
543
|
+
return (Number(dep.yellowCards) || 0) + (Number(dep.redCards) || 0);
|
|
420
544
|
}
|
|
421
545
|
```
|
|
422
546
|
|
|
423
547
|
### Conditional Text
|
|
424
548
|
|
|
425
|
-
```
|
|
426
|
-
|
|
549
|
+
```typescript
|
|
550
|
+
interface Dependencies {
|
|
551
|
+
dueDate: number;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function status_XXX(dep: Dependencies): string {
|
|
427
555
|
if (!dep.dueDate) return "No due date";
|
|
428
556
|
if (dep.dueDate < Date.now()) return "Overdue";
|
|
429
557
|
return "On track";
|
|
430
558
|
}
|
|
431
559
|
```
|
|
432
560
|
|
|
433
|
-
###
|
|
561
|
+
### Exclude Multiple Phases
|
|
434
562
|
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
563
|
+
```typescript
|
|
564
|
+
interface Dependencies {
|
|
565
|
+
prices: Array<number>;
|
|
566
|
+
rowMeta: Array<ActivityMeta>;
|
|
567
|
+
cancelledPhase: string;
|
|
568
|
+
deletedPhase: string;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function sum_excluding_cancelled_XXX(dep: Dependencies): number {
|
|
572
|
+
const prices = dep.prices || [];
|
|
573
|
+
const rowMeta = dep.rowMeta || [];
|
|
574
|
+
|
|
575
|
+
let total = 0;
|
|
576
|
+
for (let i = 0; i < prices.length; i++) {
|
|
577
|
+
const phase = rowMeta[i] ? rowMeta[i].phase : null;
|
|
578
|
+
if (phase === dep.cancelledPhase || phase === dep.deletedPhase) continue;
|
|
579
|
+
total += Number(prices[i]) || 0;
|
|
441
580
|
}
|
|
581
|
+
return total;
|
|
442
582
|
}
|
|
443
583
|
```
|
|
444
584
|
|
|
445
|
-
### Emoji Based on Own Phase
|
|
585
|
+
### Emoji Name Function Based on Own Phase
|
|
446
586
|
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
data: ["meta"] // Returns this activity's metadata
|
|
453
|
-
},
|
|
454
|
-
donePhase: {
|
|
455
|
-
type: "?",
|
|
456
|
-
data: [WorkflowIds.this_workflow, PhaseIds.done]
|
|
457
|
-
}
|
|
587
|
+
```typescript
|
|
588
|
+
interface Dependencies {
|
|
589
|
+
myMeta: ActivityMeta;
|
|
590
|
+
donePhase: string;
|
|
591
|
+
activityName: string;
|
|
458
592
|
}
|
|
459
593
|
|
|
460
|
-
|
|
461
|
-
function nameWithEmoji(dep) {
|
|
594
|
+
export function name_with_status_XXX(dep: Dependencies): string {
|
|
462
595
|
const myPhase = dep.myMeta ? dep.myMeta.phase : null;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
const prefix = (myPhase === donePhase) ? "✅ " : "🔄 ";
|
|
466
|
-
return prefix + dep.activityName;
|
|
596
|
+
const prefix = (myPhase === dep.donePhase) ? "done " : "";
|
|
597
|
+
return prefix + (dep.activityName || "Unnamed");
|
|
467
598
|
}
|
|
468
599
|
```
|
|
469
600
|
|
|
470
|
-
**Key:** Compare against the activity's OWN phase (from `"meta"` metadata), not the parent workflow's phase.
|
|
471
|
-
|
|
472
601
|
---
|
|
473
602
|
|
|
474
603
|
## Critical Rules
|
|
475
604
|
|
|
476
|
-
1. **
|
|
477
|
-
2. **Never return null
|
|
478
|
-
3. **Handle null inputs**
|
|
479
|
-
4. **Same inputs
|
|
480
|
-
5. **
|
|
481
|
-
6. **
|
|
482
|
-
7. **
|
|
483
|
-
8. **
|
|
484
|
-
9. **
|
|
485
|
-
10. **
|
|
605
|
+
1. **TypeScript files, JavaScript runtime** — Write `.ts` with interfaces for dev safety. Runtime strips TS and runs ES6 JS in isolated-vm. No TS-only syntax in function bodies.
|
|
606
|
+
2. **Never return `null`/`undefined` from numeric fields** — Always return `0` as default. Hailer rejects empty string `''` with "value is not allowed to be empty" — return `'[]'` or valid non-empty string for text fields.
|
|
607
|
+
3. **Handle null inputs defensively** — Use `Number(x) || 0` for numbers, `|| ""` for strings, `|| []` for arrays.
|
|
608
|
+
4. **Deterministic** — Same inputs must produce same outputs. No `Math.random()`, no non-deterministic behavior.
|
|
609
|
+
5. **No unused dependencies** — Every `functionVariables` entry must be used in the function. Unused deps cause wasted cross-workflow lookups.
|
|
610
|
+
6. **Correct types in Dependencies** — Match the source field type exactly. A numeric source field is `number`, not `string`.
|
|
611
|
+
7. **Return type is never `any`** — Always declare the actual return type (`number`, `string`).
|
|
612
|
+
8. **Use enums for IDs** — Never hardcode workflow/field/phase IDs. Import from `workspace/enums.ts`.
|
|
613
|
+
9. **`fields-push:force` only** — Non-force push skips `functionVariables` changes.
|
|
614
|
+
10. **Pull after push** — Always `npm run pull` to get server-assigned IDs.
|
|
615
|
+
11. **Magic numbers as constants** — Weights, multipliers, penalties should be named `const` at the top of the function.
|
|
616
|
+
12. **`linkedfrom` fields don't work in isolated-vm** — Use `<` backlink dependencies instead.
|
|
486
617
|
|
|
487
618
|
---
|
|
488
619
|
|
|
489
620
|
## Common Mistakes
|
|
490
621
|
|
|
491
|
-
| Wrong | Right |
|
|
492
|
-
|
|
493
|
-
| `
|
|
494
|
-
| `arr[i] * 2` | `(Number(arr[i]) \|\| 0) * 2` |
|
|
495
|
-
| `
|
|
496
|
-
|
|
|
497
|
-
|
|
|
622
|
+
| Wrong | Right | Why |
|
|
623
|
+
|-------|-------|-----|
|
|
624
|
+
| `dep.arr` without default | `dep.arr \|\| []` | Backlinks can be undefined |
|
|
625
|
+
| `arr[i] * 2` | `(Number(arr[i]) \|\| 0) * 2` | Array values can be null |
|
|
626
|
+
| `meta.phase` | `meta ? meta.phase : null` | Metadata entries can be null |
|
|
627
|
+
| `minutesPlayed: string` | `minutesPlayed: number` | Source is numeric field |
|
|
628
|
+
| `): any` | `): number` | Explicit return type |
|
|
629
|
+
| Unused dep in interface | Remove from both interface AND functionVariables | Wasted lookups |
|
|
630
|
+
| `dep.salary` declared, never used | Don't declare it | Dead dependency |
|
|
631
|
+
| Hardcoded `"627d14..."` | Use `PhaseIds.active` enum | Brittle, non-portable |
|
|
632
|
+
| `npm run fields-push` | `npm run fields-push:force` | Non-force skips functionVariables |
|
|
633
|
+
| Assuming cross-workflow array alignment | Only same-workflow `<` arrays are parallel | Different workflows = independent arrays |
|
|
498
634
|
|
|
499
635
|
---
|
|
500
636
|
|
|
501
637
|
## Checklist
|
|
502
638
|
|
|
503
|
-
Before
|
|
504
|
-
|
|
505
|
-
- [ ]
|
|
506
|
-
- [ ]
|
|
507
|
-
- [ ]
|
|
508
|
-
- [ ]
|
|
509
|
-
- [ ]
|
|
510
|
-
- [ ]
|
|
639
|
+
Before pushing a function field:
|
|
640
|
+
|
|
641
|
+
- [ ] `Dependencies` interface types match source field types exactly
|
|
642
|
+
- [ ] No unused dependencies (every functionVariables entry is used)
|
|
643
|
+
- [ ] Return type is explicit (`number`, `string`), never `any`
|
|
644
|
+
- [ ] All `<` arrays default with `|| []`
|
|
645
|
+
- [ ] All numeric values wrapped with `Number(x) || 0`
|
|
646
|
+
- [ ] All metadata access null-checked (`meta ? meta.phase : null`)
|
|
647
|
+
- [ ] Phase comparisons use `?` static variables with enums
|
|
648
|
+
- [ ] All IDs come from enums, never hardcoded
|
|
649
|
+
- [ ] Magic numbers extracted to named constants
|
|
650
|
+
- [ ] Function name matches `@function:` reference in fields.ts
|
|
651
|
+
- [ ] Exported from `functions/index.ts`
|
|
652
|
+
- [ ] Tested with Vitest (happy path + null/undefined inputs)
|
|
653
|
+
- [ ] Using `fields-push:force` (not `fields-push`)
|
|
654
|
+
- [ ] Will run `npm run pull` after push
|