@rippling/rippling-sdk 0.2.0-alpha.105 → 0.2.0-alpha.106

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 (175) hide show
  1. package/client.d.mts +6 -0
  2. package/client.d.mts.map +1 -1
  3. package/client.d.ts +6 -0
  4. package/client.d.ts.map +1 -1
  5. package/client.js +6 -0
  6. package/client.js.map +1 -1
  7. package/client.mjs +6 -0
  8. package/client.mjs.map +1 -1
  9. package/lib/charts/builders.d.mts +43 -0
  10. package/lib/charts/builders.d.mts.map +1 -0
  11. package/lib/charts/builders.d.ts +43 -0
  12. package/lib/charts/builders.d.ts.map +1 -0
  13. package/lib/charts/builders.js +77 -0
  14. package/lib/charts/builders.js.map +1 -0
  15. package/lib/charts/builders.mjs +70 -0
  16. package/lib/charts/builders.mjs.map +1 -0
  17. package/lib/charts/describe.d.mts +12 -0
  18. package/lib/charts/describe.d.mts.map +1 -0
  19. package/lib/charts/describe.d.ts +12 -0
  20. package/lib/charts/describe.d.ts.map +1 -0
  21. package/lib/charts/describe.js +64 -0
  22. package/lib/charts/describe.js.map +1 -0
  23. package/lib/charts/describe.mjs +61 -0
  24. package/lib/charts/describe.mjs.map +1 -0
  25. package/lib/charts/errors.d.mts +35 -0
  26. package/lib/charts/errors.d.mts.map +1 -0
  27. package/lib/charts/errors.d.ts +35 -0
  28. package/lib/charts/errors.d.ts.map +1 -0
  29. package/lib/charts/errors.js +55 -0
  30. package/lib/charts/errors.js.map +1 -0
  31. package/lib/charts/errors.mjs +47 -0
  32. package/lib/charts/errors.mjs.map +1 -0
  33. package/lib/charts/evaluate.d.mts +16 -0
  34. package/lib/charts/evaluate.d.mts.map +1 -0
  35. package/lib/charts/evaluate.d.ts +16 -0
  36. package/lib/charts/evaluate.d.ts.map +1 -0
  37. package/lib/charts/evaluate.js +99 -0
  38. package/lib/charts/evaluate.js.map +1 -0
  39. package/lib/charts/evaluate.mjs +96 -0
  40. package/lib/charts/evaluate.mjs.map +1 -0
  41. package/lib/charts/guards.d.mts +15 -0
  42. package/lib/charts/guards.d.mts.map +1 -0
  43. package/lib/charts/guards.d.ts +15 -0
  44. package/lib/charts/guards.d.ts.map +1 -0
  45. package/lib/charts/guards.js +38 -0
  46. package/lib/charts/guards.js.map +1 -0
  47. package/lib/charts/guards.mjs +34 -0
  48. package/lib/charts/guards.mjs.map +1 -0
  49. package/lib/charts/schema.d.mts +101 -0
  50. package/lib/charts/schema.d.mts.map +1 -0
  51. package/lib/charts/schema.d.ts +101 -0
  52. package/lib/charts/schema.d.ts.map +1 -0
  53. package/lib/charts/schema.js +74 -0
  54. package/lib/charts/schema.js.map +1 -0
  55. package/lib/charts/schema.mjs +71 -0
  56. package/lib/charts/schema.mjs.map +1 -0
  57. package/lib/charts/types.d.mts +79 -0
  58. package/lib/charts/types.d.mts.map +1 -0
  59. package/lib/charts/types.d.ts +79 -0
  60. package/lib/charts/types.d.ts.map +1 -0
  61. package/lib/charts/types.js +25 -0
  62. package/lib/charts/types.js.map +1 -0
  63. package/lib/charts/types.mjs +22 -0
  64. package/lib/charts/types.mjs.map +1 -0
  65. package/lib/charts.d.mts +7 -0
  66. package/lib/charts.d.mts.map +1 -0
  67. package/lib/charts.d.ts +7 -0
  68. package/lib/charts.d.ts.map +1 -0
  69. package/lib/charts.js +32 -0
  70. package/lib/charts.js.map +1 -0
  71. package/lib/charts.mjs +7 -0
  72. package/lib/charts.mjs.map +1 -0
  73. package/package.json +1 -1
  74. package/resources/hire-requirements.d.mts +8 -74
  75. package/resources/hire-requirements.d.mts.map +1 -1
  76. package/resources/hire-requirements.d.ts +8 -74
  77. package/resources/hire-requirements.d.ts.map +1 -1
  78. package/resources/hire-requirements.js +3 -19
  79. package/resources/hire-requirements.js.map +1 -1
  80. package/resources/hire-requirements.mjs +3 -19
  81. package/resources/hire-requirements.mjs.map +1 -1
  82. package/resources/hires.d.mts +677 -142
  83. package/resources/hires.d.mts.map +1 -1
  84. package/resources/hires.d.ts +677 -142
  85. package/resources/hires.d.ts.map +1 -1
  86. package/resources/hires.js +3 -4
  87. package/resources/hires.js.map +1 -1
  88. package/resources/hires.mjs +3 -4
  89. package/resources/hires.mjs.map +1 -1
  90. package/resources/index.d.mts +1 -0
  91. package/resources/index.d.mts.map +1 -1
  92. package/resources/index.d.ts +1 -0
  93. package/resources/index.d.ts.map +1 -1
  94. package/resources/index.js +4 -2
  95. package/resources/index.js.map +1 -1
  96. package/resources/index.mjs +1 -0
  97. package/resources/index.mjs.map +1 -1
  98. package/resources/payroll-payment-mode-changes.d.mts +102 -0
  99. package/resources/payroll-payment-mode-changes.d.mts.map +1 -0
  100. package/resources/payroll-payment-mode-changes.d.ts +102 -0
  101. package/resources/payroll-payment-mode-changes.d.ts.map +1 -0
  102. package/resources/payroll-payment-mode-changes.js +54 -0
  103. package/resources/payroll-payment-mode-changes.js.map +1 -0
  104. package/resources/payroll-payment-mode-changes.mjs +50 -0
  105. package/resources/payroll-payment-mode-changes.mjs.map +1 -0
  106. package/resources/payroll-runs/index.d.mts +1 -0
  107. package/resources/payroll-runs/index.d.mts.map +1 -1
  108. package/resources/payroll-runs/index.d.ts +1 -0
  109. package/resources/payroll-runs/index.d.ts.map +1 -1
  110. package/resources/payroll-runs/index.js +3 -1
  111. package/resources/payroll-runs/index.js.map +1 -1
  112. package/resources/payroll-runs/index.mjs +1 -0
  113. package/resources/payroll-runs/index.mjs.map +1 -1
  114. package/resources/payroll-runs/off-cycle.d.mts +322 -0
  115. package/resources/payroll-runs/off-cycle.d.mts.map +1 -0
  116. package/resources/payroll-runs/off-cycle.d.ts +322 -0
  117. package/resources/payroll-runs/off-cycle.d.ts.map +1 -0
  118. package/resources/payroll-runs/off-cycle.js +112 -0
  119. package/resources/payroll-runs/off-cycle.js.map +1 -0
  120. package/resources/payroll-runs/off-cycle.mjs +108 -0
  121. package/resources/payroll-runs/off-cycle.mjs.map +1 -0
  122. package/resources/payroll-runs/payroll-runs.d.mts +4 -0
  123. package/resources/payroll-runs/payroll-runs.d.mts.map +1 -1
  124. package/resources/payroll-runs/payroll-runs.d.ts +4 -0
  125. package/resources/payroll-runs/payroll-runs.d.ts.map +1 -1
  126. package/resources/payroll-runs/payroll-runs.js +4 -0
  127. package/resources/payroll-runs/payroll-runs.js.map +1 -1
  128. package/resources/payroll-runs/payroll-runs.mjs +4 -0
  129. package/resources/payroll-runs/payroll-runs.mjs.map +1 -1
  130. package/resources/sandboxes.d.mts +17 -11
  131. package/resources/sandboxes.d.mts.map +1 -1
  132. package/resources/sandboxes.d.ts +17 -11
  133. package/resources/sandboxes.d.ts.map +1 -1
  134. package/resources/sandboxes.js +1 -3
  135. package/resources/sandboxes.js.map +1 -1
  136. package/resources/sandboxes.mjs +1 -3
  137. package/resources/sandboxes.mjs.map +1 -1
  138. package/resources/variable-compensation-payout-labels.d.mts +1 -1
  139. package/resources/variable-compensation-payout-labels.d.ts +1 -1
  140. package/resources/variable-compensation-payout-labels.js +1 -1
  141. package/resources/variable-compensation-payout-labels.mjs +1 -1
  142. package/resources/variable-compensation-payout-types.d.mts +1 -1
  143. package/resources/variable-compensation-payout-types.d.ts +1 -1
  144. package/resources/variable-compensation-payout-types.js +1 -1
  145. package/resources/variable-compensation-payout-types.mjs +1 -1
  146. package/resources/variable-compensation-payouts.d.mts +1 -1
  147. package/resources/variable-compensation-payouts.d.ts +1 -1
  148. package/resources/variable-compensation-payouts.js +1 -1
  149. package/resources/variable-compensation-payouts.mjs +1 -1
  150. package/src/client.ts +16 -0
  151. package/src/lib/charts/README.md +114 -0
  152. package/src/lib/charts/builders.ts +90 -0
  153. package/src/lib/charts/describe.ts +67 -0
  154. package/src/lib/charts/errors.ts +48 -0
  155. package/src/lib/charts/evaluate.ts +114 -0
  156. package/src/lib/charts/guards.ts +42 -0
  157. package/src/lib/charts/schema.ts +72 -0
  158. package/src/lib/charts/types.ts +113 -0
  159. package/src/lib/charts.ts +43 -0
  160. package/src/resources/hire-requirements.ts +8 -85
  161. package/src/resources/hires.ts +866 -213
  162. package/src/resources/index.ts +5 -0
  163. package/src/resources/payroll-payment-mode-changes.ts +121 -0
  164. package/src/resources/payroll-runs/index.ts +1 -0
  165. package/src/resources/payroll-runs/off-cycle.ts +372 -0
  166. package/src/resources/payroll-runs/payroll-runs.ts +10 -0
  167. package/src/resources/sandboxes.ts +17 -11
  168. package/src/resources/variable-compensation-payout-labels.ts +1 -1
  169. package/src/resources/variable-compensation-payout-types.ts +1 -1
  170. package/src/resources/variable-compensation-payouts.ts +1 -1
  171. package/src/version.ts +1 -1
  172. package/version.d.mts +1 -1
  173. package/version.d.ts +1 -1
  174. package/version.js +1 -1
  175. package/version.mjs +1 -1
@@ -0,0 +1,90 @@
1
+ import { ChartRuleError } from './errors';
2
+ import {
3
+ type ComparisonKind,
4
+ type ComparisonNode,
5
+ type IfElseNode,
6
+ type LiteralNode,
7
+ type RefNode,
8
+ type RuleNode,
9
+ type RulePlaceholder,
10
+ type Scalar,
11
+ } from './types';
12
+
13
+ /** A rule node, a nested rule (`{$rule}` placeholder), or a bare scalar. */
14
+ export type Operand = RuleNode | RulePlaceholder | Scalar;
15
+
16
+ function isRuleNode(operand: Operand): operand is RuleNode {
17
+ return typeof operand === 'object' && operand !== null && 'kind' in operand;
18
+ }
19
+
20
+ /** True when a value is a `{$rule}` placeholder — the marker the builders emit. */
21
+ export function isRulePlaceholder(value: unknown): value is RulePlaceholder {
22
+ return typeof value === 'object' && value !== null && '$rule' in value;
23
+ }
24
+
25
+ /** Wrap a bare scalar as a literal node. */
26
+ export function literal(value: Scalar): LiteralNode {
27
+ return { kind: 'literal', value };
28
+ }
29
+
30
+ function toNode(operand: Operand): RuleNode {
31
+ if (isRulePlaceholder(operand)) {
32
+ return operand.$rule; // a nested rule — unwrap it, never wrap it as a literal
33
+ }
34
+ return isRuleNode(operand) ? operand : literal(operand);
35
+ }
36
+
37
+ /**
38
+ * `value` — this data point's plotted numeric value: a bar's height, a point's
39
+ * y, a slice's amount. It is what a colour rule compares against a constant.
40
+ */
41
+ export const value: RefNode = Object.freeze({ kind: 'value' });
42
+
43
+ /* Comparisons — return a condition. */
44
+ function comparison(kind: ComparisonKind): (left: Operand, right: Operand) => ComparisonNode {
45
+ return (left, right) => ({ kind, left: toNode(left), right: toNode(right) });
46
+ }
47
+ export const equals = comparison('equals');
48
+ export const isNot = comparison('isNot');
49
+ export const lessThan = comparison('lessThan');
50
+ export const greaterThan = comparison('greaterThan');
51
+ export const lessOrEqual = comparison('lessOrEqual');
52
+ export const greaterOrEqual = comparison('greaterOrEqual');
53
+
54
+ /**
55
+ * A conditional value: pick `ifTrue` when the condition holds, else `ifFalse`.
56
+ * Channel-agnostic — wrap it in a channel builder (`color`) to target something.
57
+ *
58
+ * when(lessThan(value, 100), '#E24B4A', '#378ADD')
59
+ * // → '#E24B4A' when this point's value is under 100, '#378ADD' otherwise
60
+ *
61
+ * `ifFalse` is optional and defaults to `null` — for a colour, that means no
62
+ * override (the chart's default is kept). Returns a plain node, not a placeholder.
63
+ */
64
+ export function when(condition: RuleNode, ifTrue: Operand, ifFalse?: Operand): IfElseNode {
65
+ if (ifTrue === undefined) {
66
+ throw new ChartRuleError('when(condition, ifTrue, ifFalse?) needs a value for the true case');
67
+ }
68
+ return {
69
+ kind: 'ifElse',
70
+ cond: condition,
71
+ then: toNode(ifTrue),
72
+ else: toNode(ifFalse ?? null),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Target the colour channel — the v1 channel. Wrap a conditional value so the
78
+ * renderer knows it resolves to a colour, and the panel knows what it edits.
79
+ *
80
+ * color(when(lessThan(value, 100), '#E24B4A', '#378ADD'))
81
+ * // → red when this point's value is under 100, blue otherwise
82
+ *
83
+ * Takes a conditional value — a `when(...)` (`IfElseNode`) or an already-wrapped
84
+ * rule — not a bare `value` or comparison: those resolve to a number or a
85
+ * boolean, which is not a colour. `evaluate` enforces the same at runtime for a
86
+ * deserialized rule. Returns a `{$rule}` placeholder tagged `channel: 'color'`.
87
+ */
88
+ export function color(rule: IfElseNode | RulePlaceholder): RulePlaceholder {
89
+ return { $rule: isRulePlaceholder(rule) ? rule.$rule : rule, channel: 'color' };
90
+ }
@@ -0,0 +1,67 @@
1
+ import { isRulePlaceholder } from './builders';
2
+ import { RuleDepthError, UnknownNodeError } from './errors';
3
+ import { assertRuleNode, literalValue } from './guards';
4
+ import { MAX_RULE_DEPTH, type Control, type Rule, type RuleNode } from './types';
5
+
6
+ /**
7
+ * Turn a rule into the shape the settings panel renders: a label plus its
8
+ * editable parts. Accepts a `{$rule}` placeholder or a bare tree. Walks the same
9
+ * tree `evaluate` runs — and validates each node the same way — so a malformed or
10
+ * unknown node throws a `ChartRuleError` here too, never a native `TypeError`.
11
+ * The unwrap is gated on `isRulePlaceholder` (not a raw `'$rule' in rule`) so a
12
+ * `null`/primitive rule reaches `assertRuleNode` instead of throwing on `in`.
13
+ * A tree nested past `MAX_RULE_DEPTH` throws `RuleDepthError`.
14
+ */
15
+ export function describe(rule: Rule): Control {
16
+ if (isRulePlaceholder(rule)) {
17
+ return visit(rule.$rule, 0);
18
+ }
19
+ return visit(rule, 0);
20
+ }
21
+
22
+ function visit(node: RuleNode, depth: number): Control {
23
+ if (depth > MAX_RULE_DEPTH) {
24
+ throw new RuleDepthError(MAX_RULE_DEPTH);
25
+ }
26
+ assertRuleNode(node);
27
+ const next = depth + 1;
28
+ switch (node.kind) {
29
+ case 'value':
30
+ return { role: 'ref', label: "this row's value" };
31
+ case 'literal': {
32
+ const scalar = literalValue(node);
33
+ return { role: 'literal', label: String(scalar), value: scalar };
34
+ }
35
+
36
+ case 'equals':
37
+ return comparison('is', node.left, node.right, next);
38
+ case 'isNot':
39
+ return comparison('is not', node.left, node.right, next);
40
+ case 'lessThan':
41
+ return comparison('is under', node.left, node.right, next);
42
+ case 'greaterThan':
43
+ return comparison('is over', node.left, node.right, next);
44
+ case 'lessOrEqual':
45
+ return comparison('is at most', node.left, node.right, next);
46
+ case 'greaterOrEqual':
47
+ return comparison('is at least', node.left, node.right, next);
48
+
49
+ case 'ifElse':
50
+ return {
51
+ role: 'rule',
52
+ label: 'when',
53
+ parts: [visit(node.cond, next), visit(node.then, next), visit(node.else, next)],
54
+ };
55
+
56
+ default:
57
+ return unreachable(node);
58
+ }
59
+ }
60
+
61
+ function comparison(label: string, left: RuleNode, right: RuleNode, depth: number): Control {
62
+ return { role: 'comparison', label, parts: [visit(left, depth), visit(right, depth)] };
63
+ }
64
+
65
+ function unreachable(node: never): never {
66
+ throw new UnknownNodeError((node as { kind?: string }).kind);
67
+ }
@@ -0,0 +1,48 @@
1
+ /** Base class for every error the chart-rule evaluator can throw. */
2
+ export class ChartRuleError extends Error {}
3
+
4
+ /**
5
+ * A node whose `kind` is not in the vocabulary. The caller catches this and
6
+ * shows the setting as "configured in code", read-only.
7
+ */
8
+ export class UnknownNodeError extends ChartRuleError {
9
+ constructor(public readonly kind?: string) {
10
+ super(`Unknown chart-rule node: ${kind ?? '(missing kind)'}`);
11
+ this.name = 'UnknownNodeError';
12
+ }
13
+ }
14
+
15
+ /** A node evaluated to the wrong type of value (e.g. comparing non-numbers). */
16
+ export class RuleTypeError extends ChartRuleError {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = 'RuleTypeError';
20
+ }
21
+ }
22
+
23
+ /**
24
+ * A structurally invalid node in a deserialized tree: not an object, no string
25
+ * `kind`, or a `literal` without a scalar `value`. Distinct from
26
+ * `UnknownNodeError` (a well-formed node whose `kind` is simply not in the
27
+ * vocabulary — the intentional escape hatch). Like the others it is a
28
+ * `ChartRuleError`, so a caller reading an untrusted rule catches it instead of
29
+ * a native `TypeError` (or, worse, an `undefined` slipping through silently).
30
+ */
31
+ export class MalformedRuleError extends ChartRuleError {
32
+ constructor(message: string) {
33
+ super(message);
34
+ this.name = 'MalformedRuleError';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A rule nested deeper than the evaluator will walk. Guards against a
40
+ * stack-overflowing tree from untrusted, deserialized input; like the others it
41
+ * is a `ChartRuleError`, so the caller's escape hatch catches it.
42
+ */
43
+ export class RuleDepthError extends ChartRuleError {
44
+ constructor(maxDepth: number) {
45
+ super(`chart rule nested deeper than ${maxDepth} levels`);
46
+ this.name = 'RuleDepthError';
47
+ }
48
+ }
@@ -0,0 +1,114 @@
1
+ import { isRulePlaceholder } from './builders';
2
+ import { MalformedRuleError, RuleDepthError, RuleTypeError, UnknownNodeError } from './errors';
3
+ import { assertRuleNode, literalValue } from './guards';
4
+ import {
5
+ MAX_RULE_DEPTH,
6
+ type Rule,
7
+ type RuleChannel,
8
+ type RuleContext,
9
+ type RuleNode,
10
+ type RuleValue,
11
+ } from './types';
12
+
13
+ /**
14
+ * Resolve a rule to a value against `ctx`. Accepts a `{$rule}` placeholder or a
15
+ * bare tree. A malformed or unknown node throws a `ChartRuleError` (never a
16
+ * native `TypeError`), so a caller reading an untrusted, deserialized rule can
17
+ * catch every failure with one type. When the rule is a colour placeholder, the
18
+ * result is checked to be a colour (`string | null`) — a rule that resolves to a
19
+ * number or boolean is a `RuleTypeError`, not something the renderer paints.
20
+ *
21
+ * Pure: the only inputs are the node and the context. No clock, no randomness,
22
+ * no I/O — which is what makes it safe to run on an untrusted, user-edited tree
23
+ * and identical across the renderer, the panel readout, and a test. A tree
24
+ * nested past `MAX_RULE_DEPTH` throws `RuleDepthError` rather than overflowing.
25
+ */
26
+ export function evaluate(rule: Rule, ctx: RuleContext): RuleValue {
27
+ if (isRulePlaceholder(rule)) {
28
+ return coerceForChannel(rule.channel, visit(rule.$rule, ctx, 0));
29
+ }
30
+ return visit(rule, ctx, 0);
31
+ }
32
+
33
+ function visit(node: RuleNode, ctx: RuleContext, depth: number): RuleValue {
34
+ if (depth > MAX_RULE_DEPTH) {
35
+ throw new RuleDepthError(MAX_RULE_DEPTH);
36
+ }
37
+ assertRuleNode(node);
38
+ const next = depth + 1;
39
+ switch (node.kind) {
40
+ case 'value':
41
+ return ctx.value;
42
+ case 'literal':
43
+ return literalValue(node);
44
+
45
+ case 'equals':
46
+ return visit(node.left, ctx, next) === visit(node.right, ctx, next);
47
+ case 'isNot':
48
+ return visit(node.left, ctx, next) !== visit(node.right, ctx, next);
49
+ case 'lessThan':
50
+ return num(visit(node.left, ctx, next)) < num(visit(node.right, ctx, next));
51
+ case 'greaterThan':
52
+ return num(visit(node.left, ctx, next)) > num(visit(node.right, ctx, next));
53
+ case 'lessOrEqual':
54
+ return num(visit(node.left, ctx, next)) <= num(visit(node.right, ctx, next));
55
+ case 'greaterOrEqual':
56
+ return num(visit(node.left, ctx, next)) >= num(visit(node.right, ctx, next));
57
+
58
+ case 'ifElse':
59
+ return bool(visit(node.cond, ctx, next)) ? visit(node.then, ctx, next) : visit(node.else, ctx, next);
60
+
61
+ default:
62
+ return unreachable(node);
63
+ }
64
+ }
65
+
66
+ /* -------------------------------------------------------------------------- */
67
+ /* Coercions — narrow a RuleValue, or throw a typed error. */
68
+ /* -------------------------------------------------------------------------- */
69
+
70
+ function num(value: RuleValue): number {
71
+ if (typeof value === 'number') {
72
+ return value;
73
+ }
74
+ throw new RuleTypeError(`expected a number, got ${typeName(value)}`);
75
+ }
76
+
77
+ function bool(value: RuleValue): boolean {
78
+ if (typeof value === 'boolean') {
79
+ return value;
80
+ }
81
+ throw new RuleTypeError(`expected a boolean, got ${typeName(value)}`);
82
+ }
83
+
84
+ /** A colour channel must resolve to a colour string, or null for "no override". */
85
+ function asColor(value: RuleValue): RuleValue {
86
+ if (value === null || typeof value === 'string') {
87
+ return value;
88
+ }
89
+ throw new RuleTypeError(`a colour rule must resolve to a colour string or null, got ${typeName(value)}`);
90
+ }
91
+
92
+ /**
93
+ * Coerce a resolved value to what its channel can use. A colour rule must
94
+ * resolve to a colour; a future channel would add its own case here. A
95
+ * placeholder whose `channel` is missing or unrecognised (only possible from
96
+ * untrusted, deserialized input — the type and schema both pin it to `'color'`)
97
+ * is malformed rather than silently unchecked.
98
+ */
99
+ function coerceForChannel(channel: RuleChannel, value: RuleValue): RuleValue {
100
+ switch (channel) {
101
+ case 'color':
102
+ return asColor(value);
103
+ default:
104
+ throw new MalformedRuleError(`a rule placeholder has an unsupported channel: ${String(channel)}`);
105
+ }
106
+ }
107
+
108
+ function typeName(value: RuleValue): string {
109
+ return value === null ? 'null' : typeof value;
110
+ }
111
+
112
+ function unreachable(node: never): never {
113
+ throw new UnknownNodeError((node as { kind?: string }).kind);
114
+ }
@@ -0,0 +1,42 @@
1
+ import { MalformedRuleError } from './errors';
2
+ import { type RuleNode, type Scalar } from './types';
3
+
4
+ /**
5
+ * Runtime validation for one node of a deserialized, untrusted rule tree, shared
6
+ * by `evaluate` and `describe`. A rule can arrive from JSON that TypeScript never
7
+ * checked, so `null`, a non-object, or a node without a string `kind` would throw
8
+ * a native `TypeError` on the first field read — skipping the `ChartRuleError`
9
+ * escape hatch a caller relies on. Validate before reading fields.
10
+ */
11
+
12
+ const SCALAR_TYPES: ReadonlySet<string> = new Set(['number', 'string', 'boolean']);
13
+
14
+ /**
15
+ * Assert `node` is shaped like a rule node — an object carrying a string `kind`.
16
+ * An unrecognised `kind` is *not* rejected here; that is the caller's `switch`
17
+ * default (`UnknownNodeError`), the deliberate "configured in code" signal.
18
+ */
19
+ export function assertRuleNode(node: unknown): asserts node is RuleNode {
20
+ if (
21
+ node === null ||
22
+ typeof node !== 'object' ||
23
+ Array.isArray(node) ||
24
+ typeof (node as { kind?: unknown }).kind !== 'string'
25
+ ) {
26
+ throw new MalformedRuleError('a rule node must be an object with a string "kind"');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Read a `literal` node's `value`, rejecting a missing or non-scalar one. Without
32
+ * this, `{ kind: 'literal' }` resolves to `undefined` and slips past the caller.
33
+ */
34
+ export function literalValue(node: { value?: unknown }): Scalar {
35
+ const { value } = node;
36
+ if (value === null || SCALAR_TYPES.has(typeof value)) {
37
+ return value as Scalar;
38
+ }
39
+ throw new MalformedRuleError(
40
+ `a literal must carry a scalar value, got ${value === undefined ? 'undefined' : typeof value}`,
41
+ );
42
+ }
@@ -0,0 +1,72 @@
1
+ import { CHART_RULE_CONTRACT_VERSION } from './types';
2
+
3
+ /** Stable identifier for the published contract, versioned by the tag. */
4
+ export const CHART_RULE_SCHEMA_ID = `https://rippling.com/schemas/${CHART_RULE_CONTRACT_VERSION}.json`;
5
+
6
+ /**
7
+ * JSON Schema (2020-12) for the `{$rule}` placeholder and its closed node set.
8
+ *
9
+ * This is the contract. A consumer that keeps its own rule evaluator (rather
10
+ * than importing `evaluate`) can validate against this so the two never drift —
11
+ * every kind and operand shape the builders can emit is described here.
12
+ */
13
+ export const chartRuleSchema = {
14
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
15
+ $id: CHART_RULE_SCHEMA_ID,
16
+ title: 'Chart rule placeholder',
17
+ description: 'A rule tree the chart agent emits into a chart option, marked with $rule.',
18
+ type: 'object',
19
+ required: ['$rule', 'channel'],
20
+ additionalProperties: false,
21
+ properties: {
22
+ // A colour rule's top-level tree is the conditional-value form (`when(...)`),
23
+ // matching what `color()` builds and accepts. A bare ref/comparison/literal
24
+ // resolves to a number/boolean/constant, not a colour decision.
25
+ $rule: { $ref: '#/$defs/ifElse' },
26
+ channel: { const: 'color' },
27
+ },
28
+ $defs: {
29
+ scalar: { type: ['number', 'string', 'boolean', 'null'] },
30
+ node: {
31
+ oneOf: [
32
+ { $ref: '#/$defs/ref' },
33
+ { $ref: '#/$defs/literal' },
34
+ { $ref: '#/$defs/comparison' },
35
+ { $ref: '#/$defs/ifElse' },
36
+ ],
37
+ },
38
+ ref: {
39
+ type: 'object',
40
+ required: ['kind'],
41
+ additionalProperties: false,
42
+ properties: { kind: { const: 'value' } },
43
+ },
44
+ literal: {
45
+ type: 'object',
46
+ required: ['kind', 'value'],
47
+ additionalProperties: false,
48
+ properties: { kind: { const: 'literal' }, value: { $ref: '#/$defs/scalar' } },
49
+ },
50
+ comparison: {
51
+ type: 'object',
52
+ required: ['kind', 'left', 'right'],
53
+ additionalProperties: false,
54
+ properties: {
55
+ kind: { enum: ['equals', 'isNot', 'lessThan', 'greaterThan', 'lessOrEqual', 'greaterOrEqual'] },
56
+ left: { $ref: '#/$defs/node' },
57
+ right: { $ref: '#/$defs/node' },
58
+ },
59
+ },
60
+ ifElse: {
61
+ type: 'object',
62
+ required: ['kind', 'cond', 'then', 'else'],
63
+ additionalProperties: false,
64
+ properties: {
65
+ kind: { const: 'ifElse' },
66
+ cond: { $ref: '#/$defs/node' },
67
+ then: { $ref: '#/$defs/node' },
68
+ else: { $ref: '#/$defs/node' },
69
+ },
70
+ },
71
+ },
72
+ } as const;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Chart rule vocabulary — shared types.
3
+ *
4
+ * A rule is a small, serializable tree of nodes discriminated by `kind`. The
5
+ * agent builds it with the helpers in `builders.ts`, which emit it wrapped in a
6
+ * `{$rule}` placeholder — the same idea as the `{$fn}` placeholders a chart
7
+ * already emits. The renderer resolves it with `evaluate`; the settings panel
8
+ * reads it with `describe`. All three agree because a node can only ever be one
9
+ * of the kinds defined here — the closed set the `chartRuleSchema` describes.
10
+ *
11
+ * v1 is scoped to conditional formatting: compare this datum's `value` against a
12
+ * constant, and (optionally) pick between two values. Richer pieces — aggregates,
13
+ * logic, more refs — are the documented roadmap in the README.
14
+ */
15
+
16
+ export const CHART_RULE_CONTRACT_VERSION = 'chart_rule.v1' as const;
17
+ export type ChartRuleContractVersion = typeof CHART_RULE_CONTRACT_VERSION;
18
+
19
+ /** A leaf value a rule can carry or resolve to. */
20
+ export type Scalar = number | string | boolean | null;
21
+
22
+ /* -------------------------------------------------------------------------- */
23
+ /* Nodes — the closed set, discriminated by `kind`. Each appears in every face. */
24
+ /* -------------------------------------------------------------------------- */
25
+
26
+ /** Reads a value out of the evaluation context — this datum's plotted value. */
27
+ export type RefNode = { kind: 'value' };
28
+
29
+ export interface LiteralNode {
30
+ kind: 'literal';
31
+ value: Scalar;
32
+ }
33
+
34
+ export type ComparisonKind =
35
+ | 'equals'
36
+ | 'isNot'
37
+ | 'lessThan'
38
+ | 'greaterThan'
39
+ | 'lessOrEqual'
40
+ | 'greaterOrEqual';
41
+ export interface ComparisonNode {
42
+ kind: ComparisonKind;
43
+ left: RuleNode;
44
+ right: RuleNode;
45
+ }
46
+
47
+ /** The conditional-value form produced by `when(cond, then, else)`. */
48
+ export interface IfElseNode {
49
+ kind: 'ifElse';
50
+ cond: RuleNode;
51
+ then: RuleNode;
52
+ else: RuleNode;
53
+ }
54
+
55
+ /** Any node of a rule tree. */
56
+ export type RuleNode = RefNode | LiteralNode | ComparisonNode | IfElseNode;
57
+
58
+ /* -------------------------------------------------------------------------- */
59
+ /* The placeholder — how a rule is embedded in a chart option (like {$fn}). */
60
+ /* -------------------------------------------------------------------------- */
61
+
62
+ /**
63
+ * The chart channel a rule drives. v1 supports colour only — a rule decides the
64
+ * colour of a series/datum. Other channels (opacity, label visibility, …) are
65
+ * the documented roadmap and would be added here.
66
+ */
67
+ export type RuleChannel = 'color';
68
+
69
+ /**
70
+ * What a builder returns: a rule tree, marked so the renderer can find it, and
71
+ * tagged with the channel it targets. `channel: 'color'` says this resolves to a
72
+ * colour — so a reader sees the target without inferring it from where it sits.
73
+ */
74
+ export interface RulePlaceholder {
75
+ $rule: RuleNode;
76
+ channel: RuleChannel;
77
+ }
78
+
79
+ /** A placeholder, or the bare tree — what `evaluate` and `describe` accept. */
80
+ export type Rule = RulePlaceholder | RuleNode;
81
+
82
+ /* -------------------------------------------------------------------------- */
83
+ /* Evaluation */
84
+ /* -------------------------------------------------------------------------- */
85
+
86
+ /** What a rule is evaluated against: the current datum's value. */
87
+ export interface RuleContext {
88
+ value: number;
89
+ }
90
+
91
+ /** The result of evaluating a node. */
92
+ export type RuleValue = Scalar;
93
+
94
+ /**
95
+ * The deepest a rule may nest before `evaluate`/`describe` refuse it. Real rules
96
+ * are a handful of levels; this only stops a pathological deserialized tree from
97
+ * overflowing the stack. Far below the JS call-stack limit.
98
+ */
99
+ export const MAX_RULE_DEPTH = 256;
100
+
101
+ /* -------------------------------------------------------------------------- */
102
+ /* Description — what the settings panel renders */
103
+ /* -------------------------------------------------------------------------- */
104
+
105
+ export type ControlRole = 'rule' | 'comparison' | 'ref' | 'literal';
106
+
107
+ /** A node as the panel should show it: a label, its editable value, its parts. */
108
+ export interface Control {
109
+ role: ControlRole;
110
+ label: string;
111
+ value?: Scalar;
112
+ parts?: Control[];
113
+ }
@@ -0,0 +1,43 @@
1
+ export {
2
+ color,
3
+ equals,
4
+ greaterOrEqual,
5
+ greaterThan,
6
+ isNot,
7
+ isRulePlaceholder,
8
+ lessOrEqual,
9
+ lessThan,
10
+ literal,
11
+ value,
12
+ when,
13
+ type Operand,
14
+ } from './charts/builders';
15
+ export { evaluate } from './charts/evaluate';
16
+ export { describe } from './charts/describe';
17
+ export { chartRuleSchema, CHART_RULE_SCHEMA_ID } from './charts/schema';
18
+ export {
19
+ ChartRuleError,
20
+ MalformedRuleError,
21
+ RuleDepthError,
22
+ RuleTypeError,
23
+ UnknownNodeError,
24
+ } from './charts/errors';
25
+ export {
26
+ CHART_RULE_CONTRACT_VERSION,
27
+ MAX_RULE_DEPTH,
28
+ type ChartRuleContractVersion,
29
+ type ComparisonKind,
30
+ type ComparisonNode,
31
+ type Control,
32
+ type ControlRole,
33
+ type IfElseNode,
34
+ type LiteralNode,
35
+ type RefNode,
36
+ type Rule,
37
+ type RuleChannel,
38
+ type RuleContext,
39
+ type RuleNode,
40
+ type RulePlaceholder,
41
+ type RuleValue,
42
+ type Scalar,
43
+ } from './charts/types';