@cxpinsight/survey-spec 0.1.0 → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CXPinsight
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@cxpinsight/survey-spec",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The survey specification the renderers share — expression evaluation first. Source of truth for behaviour that must be identical on web, links and mobile.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist/",
9
- "src/",
10
- "README.md"
9
+ "README.md",
10
+ "LICENSE"
11
11
  ],
12
12
  "scripts": {
13
13
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
14
14
  "typecheck": "tsc --noEmit",
15
15
  "test": "node --test test/*.test.js",
16
- "prepublishOnly": "npm run typecheck && npm run build"
16
+ "prepublishOnly": "node scripts/check-toolchain.js && npm run typecheck && npm run build"
17
17
  },
18
18
  "devDependencies": {
19
19
  "typescript": ">=5.0.0"
@@ -1,7 +0,0 @@
1
- export type { AnswersMap, VariablesMap, EvalContext } from './types';
2
- import type { EvalContext } from './types';
3
- export declare function evaluateExpression(source: string, ctx: EvalContext): unknown;
4
- export declare function evaluateCondition(source: string | undefined, ctx: EvalContext): boolean;
5
- /** Whether `source` parses at all, independent of what it evaluates to. */
6
- export declare function isParseable(source: string | undefined): boolean;
7
- //# sourceMappingURL=expressionEval.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"expressionEval.d.ts","sourceRoot":"","sources":["expressionEval.ts"],"names":[],"mappings":"AAwBA,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAY5E;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAcvF;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS/D"}
@@ -1,337 +0,0 @@
1
- "use strict";
2
- // Expression evaluator — supports the subset of operators the spec
3
- // commits to (spec §7, §22):
4
- //
5
- // Comparators: = != < > <= >=
6
- // Boolean: and or not && || !
7
- // Arithmetic: + - * /
8
- // Refs: {q_id} the respondent's answer to q_id
9
- // {{variable}} resolved variable value
10
- // 'string' string literal
11
- // 42 numeric literal
12
- // true / false boolean literal
13
- //
14
- // This is deliberately a simple tokens-based evaluator, not a full
15
- // JS-compatible expression engine. It's used for:
16
- // - showWhen / enabledWhen / requiredWhen (question + section)
17
- // - `expression` type questions (computed value)
18
- // - `calculated` variable expressions
19
- //
20
- // Being cautious: an untyped eval() would let survey authors run
21
- // arbitrary JS against respondent answers. This restricted grammar
22
- // keeps the surface area bounded.
23
- Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.evaluateExpression = evaluateExpression;
25
- exports.evaluateCondition = evaluateCondition;
26
- exports.isParseable = isParseable;
27
- // ─── Public entry point ───────────────────────────────────────────────
28
- function evaluateExpression(source, ctx) {
29
- if (typeof source !== 'string')
30
- return undefined;
31
- const src = source.trim();
32
- if (!src)
33
- return undefined;
34
- try {
35
- const tokens = tokenize(src);
36
- const [value, consumed] = parseExpression(tokens, 0, ctx);
37
- if (consumed !== tokens.length)
38
- return undefined; // trailing garbage
39
- return value;
40
- }
41
- catch {
42
- return undefined;
43
- }
44
- }
45
- // showWhen/enabledWhen coerce the result to boolean. Empty / undefined
46
- // evaluates to false (hides the question) — friendlier than throwing.
47
- function evaluateCondition(source, ctx) {
48
- if (!source)
49
- return true; // no condition = always shown
50
- // An expression that cannot be PARSED is not a false condition — it is not a
51
- // condition. Treating a typo as "false" hid the question, and a hidden question
52
- // is invisible in the response data: no answers, no error, no signal, and the
53
- // author concludes nobody wanted to answer it. Silent data loss is the worse
54
- // failure, so an invalid condition is ignored and the question shows. The
55
- // builder is where a malformed expression should be surfaced, before it ships.
56
- //
57
- // A well-formed expression that evaluates falsy still hides the question, which
58
- // is the whole point of showWhen. Only unparseable input is ignored.
59
- if (!isParseable(source))
60
- return true;
61
- const result = evaluateExpression(source, ctx);
62
- return truthy(result);
63
- }
64
- /** Whether `source` parses at all, independent of what it evaluates to. */
65
- function isParseable(source) {
66
- if (!source || !source.trim())
67
- return true;
68
- try {
69
- const tokens = tokenize(source.trim());
70
- const [, consumed] = parseExpression(tokens, 0, { variables: {}, answers: {} });
71
- return consumed === tokens.length;
72
- }
73
- catch {
74
- return false;
75
- }
76
- }
77
- function tokenize(src) {
78
- const tokens = [];
79
- let i = 0;
80
- const len = src.length;
81
- while (i < len) {
82
- const c = src[i];
83
- // whitespace
84
- if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
85
- i++;
86
- continue;
87
- }
88
- // string literal — single or double quoted
89
- if (c === "'" || c === '"') {
90
- const quote = c;
91
- let out = '';
92
- i++;
93
- while (i < len && src[i] !== quote) {
94
- if (src[i] === '\\' && i + 1 < len) {
95
- out += src[i + 1];
96
- i += 2;
97
- }
98
- else {
99
- out += src[i];
100
- i++;
101
- }
102
- }
103
- i++; // closing quote
104
- tokens.push({ kind: 'str', value: out });
105
- continue;
106
- }
107
- // numeric literal
108
- if ((c >= '0' && c <= '9') || (c === '-' && src[i + 1] >= '0' && src[i + 1] <= '9' && !prevExpectsBinary(tokens))) {
109
- let j = i;
110
- if (src[j] === '-')
111
- j++;
112
- while (j < len && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.'))
113
- j++;
114
- tokens.push({ kind: 'num', value: parseFloat(src.slice(i, j)) });
115
- i = j;
116
- continue;
117
- }
118
- // {{variable_ref}} — spec §22 interpolation shape
119
- if (c === '{' && src[i + 1] === '{') {
120
- const end = src.indexOf('}}', i + 2);
121
- if (end < 0)
122
- throw new Error('unterminated {{');
123
- const inner = src.slice(i + 2, end).trim();
124
- const dot = inner.indexOf('.');
125
- if (dot > 0) {
126
- tokens.push({ kind: 'variable_ref', name: inner.slice(0, dot).trim(), suffix: inner.slice(dot + 1).trim() });
127
- }
128
- else {
129
- tokens.push({ kind: 'variable_ref', name: inner });
130
- }
131
- i = end + 2;
132
- continue;
133
- }
134
- // {q_id} — answer reference (single-brace, spec §7)
135
- if (c === '{') {
136
- const end = src.indexOf('}', i + 1);
137
- if (end < 0)
138
- throw new Error('unterminated {');
139
- tokens.push({ kind: 'answer_ref', name: src.slice(i + 1, end).trim() });
140
- i = end + 1;
141
- continue;
142
- }
143
- // parentheses
144
- if (c === '(') {
145
- tokens.push({ kind: 'lparen' });
146
- i++;
147
- continue;
148
- }
149
- if (c === ')') {
150
- tokens.push({ kind: 'rparen' });
151
- i++;
152
- continue;
153
- }
154
- // Multi-character operators. Order matters — longest first.
155
- //
156
- // The "did we consume one?" check used to be `src[i - 1] === src[i]`, which
157
- // is not that question. After `<=` is consumed, `c` still holds `<` from the
158
- // top of the loop, so the single-character branch below pushed a SECOND `<`
159
- // token and advanced i again. The extra token made `consumed !== tokens.length`
160
- // in evaluateExpression, which returns undefined, which is falsy — so every
161
- // showWhen using <=, >=, ==, <> , && or || silently evaluated to FALSE and hid
162
- // its question. Only `!=` survived, by accident of token ordering.
163
- //
164
- // This is the live respondent path for link surveys and SdkSurveyHost, while
165
- // the mobile and web SDKs both tokenize correctly — so the same survey branched
166
- // differently depending on how a respondent arrived. Mobile already carries
167
- // this fix (survey/expressionEval.ts); this is that fix, ported back.
168
- let matchedMulti = false;
169
- for (const op of ['<=', '>=', '!=', '<>', '&&', '||', '==']) {
170
- if (src.slice(i, i + op.length) === op) {
171
- tokens.push({ kind: 'op', value: op === '==' ? '=' : op });
172
- i += op.length;
173
- matchedMulti = true;
174
- break;
175
- }
176
- }
177
- if (matchedMulti)
178
- continue;
179
- // Single-char operators
180
- if ('=<>+-*/!'.includes(c)) {
181
- tokens.push({ kind: 'op', value: c });
182
- i++;
183
- continue;
184
- }
185
- // Keywords: true / false / null / and / or / not / contains
186
- const kw = src.slice(i).match(/^[a-zA-Z_][a-zA-Z_0-9]*/);
187
- if (kw) {
188
- const word = kw[0];
189
- i += word.length;
190
- const lower = word.toLowerCase();
191
- if (lower === 'true')
192
- tokens.push({ kind: 'bool', value: true });
193
- else if (lower === 'false')
194
- tokens.push({ kind: 'bool', value: false });
195
- else if (lower === 'null')
196
- tokens.push({ kind: 'null' });
197
- else if (lower === 'and')
198
- tokens.push({ kind: 'op', value: '&&' });
199
- else if (lower === 'or')
200
- tokens.push({ kind: 'op', value: '||' });
201
- else if (lower === 'not')
202
- tokens.push({ kind: 'op', value: '!' });
203
- else if (lower === 'contains')
204
- tokens.push({ kind: 'op', value: 'contains' });
205
- else
206
- throw new Error(`unknown identifier "${word}" — refs must be wrapped in {}`);
207
- continue;
208
- }
209
- throw new Error(`unexpected character "${c}" at position ${i}`);
210
- }
211
- return tokens;
212
- }
213
- function prevExpectsBinary(tokens) {
214
- const prev = tokens[tokens.length - 1];
215
- if (!prev)
216
- return false;
217
- return prev.kind === 'num' || prev.kind === 'str' || prev.kind === 'bool'
218
- || prev.kind === 'answer_ref' || prev.kind === 'variable_ref'
219
- || prev.kind === 'rparen';
220
- }
221
- // ─── Parser — precedence climbing ────────────────────────────────────
222
- // Operator precedence table — higher number = tighter binding.
223
- const PREC = {
224
- '||': 1, '&&': 2,
225
- '=': 3, '!=': 3, '<>': 3, 'contains': 3,
226
- '<': 4, '>': 4, '<=': 4, '>=': 4,
227
- '+': 5, '-': 5,
228
- '*': 6, '/': 6,
229
- };
230
- function parseExpression(tokens, pos, ctx, minPrec = 1) {
231
- let [lhs, next] = parseUnary(tokens, pos, ctx);
232
- while (next < tokens.length) {
233
- const t = tokens[next];
234
- if (t.kind !== 'op')
235
- break;
236
- const prec = PREC[t.value];
237
- if (prec === undefined || prec < minPrec)
238
- break;
239
- const op = t.value;
240
- const [rhs, after] = parseExpression(tokens, next + 1, ctx, prec + 1);
241
- lhs = applyBinary(op, lhs, rhs);
242
- next = after;
243
- }
244
- return [lhs, next];
245
- }
246
- function parseUnary(tokens, pos, ctx) {
247
- const t = tokens[pos];
248
- if (!t)
249
- throw new Error('unexpected end of expression');
250
- if (t.kind === 'op' && t.value === '!') {
251
- const [inner, after] = parseUnary(tokens, pos + 1, ctx);
252
- return [!truthy(inner), after];
253
- }
254
- if (t.kind === 'op' && t.value === '-') {
255
- const [inner, after] = parseUnary(tokens, pos + 1, ctx);
256
- return [-Number(inner), after];
257
- }
258
- return parsePrimary(tokens, pos, ctx);
259
- }
260
- function parsePrimary(tokens, pos, ctx) {
261
- const t = tokens[pos];
262
- if (!t)
263
- throw new Error('unexpected end of expression');
264
- switch (t.kind) {
265
- case 'num': return [t.value, pos + 1];
266
- case 'str': return [t.value, pos + 1];
267
- case 'bool': return [t.value, pos + 1];
268
- case 'null': return [null, pos + 1];
269
- case 'answer_ref': return [ctx.answers[t.name], pos + 1];
270
- case 'variable_ref': {
271
- // Resolution rules for {{name.suffix}}:
272
- // • {{q1.answer}} → ctx.answers.q1 (special-case for legacy)
273
- // • {{customer_name}} → ctx.variables.customer_name
274
- // • {{device.theme}} → walk dotted path into ctx.variables.device
275
- // • {{a.b.c.d}} → walk into ctx.variables.a.b.c.d
276
- // A dotted suffix that hits a non-object mid-walk returns undefined —
277
- // which then compares falsy in showWhen. This lets authors reference
278
- // any structured runtime signal (device, session, session.location, …)
279
- // without adding new token kinds — they just live under variables.*
280
- // as nested objects, injected by the SDK at render time.
281
- if (t.suffix === 'answer')
282
- return [ctx.answers[t.name], pos + 1];
283
- const root = ctx.variables[t.name];
284
- if (!t.suffix)
285
- return [root, pos + 1];
286
- let cursor = root;
287
- for (const seg of t.suffix.split('.')) {
288
- if (cursor == null || typeof cursor !== 'object') {
289
- cursor = undefined;
290
- break;
291
- }
292
- cursor = cursor[seg];
293
- }
294
- return [cursor, pos + 1];
295
- }
296
- case 'lparen': {
297
- const [inner, after] = parseExpression(tokens, pos + 1, ctx, 1);
298
- if (tokens[after]?.kind !== 'rparen')
299
- throw new Error('missing closing paren');
300
- return [inner, after + 1];
301
- }
302
- default: throw new Error(`unexpected token: ${JSON.stringify(t)}`);
303
- }
304
- }
305
- // ─── Operators ───────────────────────────────────────────────────────
306
- function applyBinary(op, lhs, rhs) {
307
- switch (op) {
308
- case '=': return String(lhs) === String(rhs);
309
- case '!=':
310
- case '<>': return String(lhs) !== String(rhs);
311
- case '<': return Number(lhs) < Number(rhs);
312
- case '>': return Number(lhs) > Number(rhs);
313
- case '<=': return Number(lhs) <= Number(rhs);
314
- case '>=': return Number(lhs) >= Number(rhs);
315
- case '&&': return truthy(lhs) && truthy(rhs);
316
- case '||': return truthy(lhs) || truthy(rhs);
317
- case '+': return typeof lhs === 'string' || typeof rhs === 'string'
318
- ? String(lhs) + String(rhs)
319
- : Number(lhs) + Number(rhs);
320
- case '-': return Number(lhs) - Number(rhs);
321
- case '*': return Number(lhs) * Number(rhs);
322
- case '/': return Number(lhs) / Number(rhs);
323
- case 'contains':
324
- if (Array.isArray(lhs))
325
- return lhs.includes(rhs);
326
- return String(lhs ?? '').includes(String(rhs));
327
- default: return undefined;
328
- }
329
- }
330
- function truthy(v) {
331
- if (v === false || v === null || v === undefined || v === 0 || v === '')
332
- return false;
333
- if (Array.isArray(v))
334
- return v.length > 0;
335
- return true;
336
- }
337
- //# sourceMappingURL=expressionEval.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"expressionEval.js","sourceRoot":"","sources":["expressionEval.ts"],"names":[],"mappings":"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"}
@@ -1,321 +0,0 @@
1
- // Expression evaluator — supports the subset of operators the spec
2
- // commits to (spec §7, §22):
3
- //
4
- // Comparators: = != < > <= >=
5
- // Boolean: and or not && || !
6
- // Arithmetic: + - * /
7
- // Refs: {q_id} the respondent's answer to q_id
8
- // {{variable}} resolved variable value
9
- // 'string' string literal
10
- // 42 numeric literal
11
- // true / false boolean literal
12
- //
13
- // This is deliberately a simple tokens-based evaluator, not a full
14
- // JS-compatible expression engine. It's used for:
15
- // - showWhen / enabledWhen / requiredWhen (question + section)
16
- // - `expression` type questions (computed value)
17
- // - `calculated` variable expressions
18
- //
19
- // Being cautious: an untyped eval() would let survey authors run
20
- // arbitrary JS against respondent answers. This restricted grammar
21
- // keeps the surface area bounded.
22
-
23
- import type { AnswersMap, VariablesMap } from './types';
24
-
25
- export type { AnswersMap, VariablesMap, EvalContext } from './types';
26
- import type { EvalContext } from './types';
27
-
28
- // ─── Public entry point ───────────────────────────────────────────────
29
-
30
- export function evaluateExpression(source: string, ctx: EvalContext): unknown {
31
- if (typeof source !== 'string') return undefined;
32
- const src = source.trim();
33
- if (!src) return undefined;
34
- try {
35
- const tokens = tokenize(src);
36
- const [value, consumed] = parseExpression(tokens, 0, ctx);
37
- if (consumed !== tokens.length) return undefined; // trailing garbage
38
- return value;
39
- } catch {
40
- return undefined;
41
- }
42
- }
43
-
44
- // showWhen/enabledWhen coerce the result to boolean. Empty / undefined
45
- // evaluates to false (hides the question) — friendlier than throwing.
46
- export function evaluateCondition(source: string | undefined, ctx: EvalContext): boolean {
47
- if (!source) return true; // no condition = always shown
48
- // An expression that cannot be PARSED is not a false condition — it is not a
49
- // condition. Treating a typo as "false" hid the question, and a hidden question
50
- // is invisible in the response data: no answers, no error, no signal, and the
51
- // author concludes nobody wanted to answer it. Silent data loss is the worse
52
- // failure, so an invalid condition is ignored and the question shows. The
53
- // builder is where a malformed expression should be surfaced, before it ships.
54
- //
55
- // A well-formed expression that evaluates falsy still hides the question, which
56
- // is the whole point of showWhen. Only unparseable input is ignored.
57
- if (!isParseable(source)) return true;
58
- const result = evaluateExpression(source, ctx);
59
- return truthy(result);
60
- }
61
-
62
- /** Whether `source` parses at all, independent of what it evaluates to. */
63
- export function isParseable(source: string | undefined): boolean {
64
- if (!source || !source.trim()) return true;
65
- try {
66
- const tokens = tokenize(source.trim());
67
- const [, consumed] = parseExpression(tokens, 0, { variables: {}, answers: {} });
68
- return consumed === tokens.length;
69
- } catch {
70
- return false;
71
- }
72
- }
73
-
74
- // ─── Tokenizer ────────────────────────────────────────────────────────
75
-
76
- type Token =
77
- | { kind: 'num'; value: number }
78
- | { kind: 'str'; value: string }
79
- | { kind: 'bool'; value: boolean }
80
- | { kind: 'null' }
81
- | { kind: 'answer_ref'; name: string } // {q_id}
82
- | { kind: 'variable_ref'; name: string; suffix?: string } // {{name}} or {{q_id.answer}}
83
- | { kind: 'op'; value: string }
84
- | { kind: 'lparen' }
85
- | { kind: 'rparen' };
86
-
87
- function tokenize(src: string): Token[] {
88
- const tokens: Token[] = [];
89
- let i = 0;
90
- const len = src.length;
91
-
92
- while (i < len) {
93
- const c = src[i];
94
-
95
- // whitespace
96
- if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
97
-
98
- // string literal — single or double quoted
99
- if (c === "'" || c === '"') {
100
- const quote = c;
101
- let out = '';
102
- i++;
103
- while (i < len && src[i] !== quote) {
104
- if (src[i] === '\\' && i + 1 < len) { out += src[i + 1]; i += 2; }
105
- else { out += src[i]; i++; }
106
- }
107
- i++; // closing quote
108
- tokens.push({ kind: 'str', value: out });
109
- continue;
110
- }
111
-
112
- // numeric literal
113
- if ((c >= '0' && c <= '9') || (c === '-' && src[i + 1] >= '0' && src[i + 1] <= '9' && !prevExpectsBinary(tokens))) {
114
- let j = i;
115
- if (src[j] === '-') j++;
116
- while (j < len && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.')) j++;
117
- tokens.push({ kind: 'num', value: parseFloat(src.slice(i, j)) });
118
- i = j;
119
- continue;
120
- }
121
-
122
- // {{variable_ref}} — spec §22 interpolation shape
123
- if (c === '{' && src[i + 1] === '{') {
124
- const end = src.indexOf('}}', i + 2);
125
- if (end < 0) throw new Error('unterminated {{');
126
- const inner = src.slice(i + 2, end).trim();
127
- const dot = inner.indexOf('.');
128
- if (dot > 0) {
129
- tokens.push({ kind: 'variable_ref', name: inner.slice(0, dot).trim(), suffix: inner.slice(dot + 1).trim() });
130
- } else {
131
- tokens.push({ kind: 'variable_ref', name: inner });
132
- }
133
- i = end + 2;
134
- continue;
135
- }
136
-
137
- // {q_id} — answer reference (single-brace, spec §7)
138
- if (c === '{') {
139
- const end = src.indexOf('}', i + 1);
140
- if (end < 0) throw new Error('unterminated {');
141
- tokens.push({ kind: 'answer_ref', name: src.slice(i + 1, end).trim() });
142
- i = end + 1;
143
- continue;
144
- }
145
-
146
- // parentheses
147
- if (c === '(') { tokens.push({ kind: 'lparen' }); i++; continue; }
148
- if (c === ')') { tokens.push({ kind: 'rparen' }); i++; continue; }
149
-
150
- // Multi-character operators. Order matters — longest first.
151
- //
152
- // The "did we consume one?" check used to be `src[i - 1] === src[i]`, which
153
- // is not that question. After `<=` is consumed, `c` still holds `<` from the
154
- // top of the loop, so the single-character branch below pushed a SECOND `<`
155
- // token and advanced i again. The extra token made `consumed !== tokens.length`
156
- // in evaluateExpression, which returns undefined, which is falsy — so every
157
- // showWhen using <=, >=, ==, <> , && or || silently evaluated to FALSE and hid
158
- // its question. Only `!=` survived, by accident of token ordering.
159
- //
160
- // This is the live respondent path for link surveys and SdkSurveyHost, while
161
- // the mobile and web SDKs both tokenize correctly — so the same survey branched
162
- // differently depending on how a respondent arrived. Mobile already carries
163
- // this fix (survey/expressionEval.ts); this is that fix, ported back.
164
- let matchedMulti = false;
165
- for (const op of ['<=', '>=', '!=', '<>', '&&', '||', '==']) {
166
- if (src.slice(i, i + op.length) === op) {
167
- tokens.push({ kind: 'op', value: op === '==' ? '=' : op });
168
- i += op.length;
169
- matchedMulti = true;
170
- break;
171
- }
172
- }
173
- if (matchedMulti) continue;
174
-
175
- // Single-char operators
176
- if ('=<>+-*/!'.includes(c)) {
177
- tokens.push({ kind: 'op', value: c });
178
- i++;
179
- continue;
180
- }
181
-
182
- // Keywords: true / false / null / and / or / not / contains
183
- const kw = src.slice(i).match(/^[a-zA-Z_][a-zA-Z_0-9]*/);
184
- if (kw) {
185
- const word = kw[0];
186
- i += word.length;
187
- const lower = word.toLowerCase();
188
- if (lower === 'true') tokens.push({ kind: 'bool', value: true });
189
- else if (lower === 'false') tokens.push({ kind: 'bool', value: false });
190
- else if (lower === 'null') tokens.push({ kind: 'null' });
191
- else if (lower === 'and') tokens.push({ kind: 'op', value: '&&' });
192
- else if (lower === 'or') tokens.push({ kind: 'op', value: '||' });
193
- else if (lower === 'not') tokens.push({ kind: 'op', value: '!' });
194
- else if (lower === 'contains') tokens.push({ kind: 'op', value: 'contains' });
195
- else throw new Error(`unknown identifier "${word}" — refs must be wrapped in {}`);
196
- continue;
197
- }
198
-
199
- throw new Error(`unexpected character "${c}" at position ${i}`);
200
- }
201
- return tokens;
202
- }
203
-
204
- function prevExpectsBinary(tokens: Token[]): boolean {
205
- const prev = tokens[tokens.length - 1];
206
- if (!prev) return false;
207
- return prev.kind === 'num' || prev.kind === 'str' || prev.kind === 'bool'
208
- || prev.kind === 'answer_ref' || prev.kind === 'variable_ref'
209
- || prev.kind === 'rparen';
210
- }
211
-
212
- // ─── Parser — precedence climbing ────────────────────────────────────
213
-
214
- // Operator precedence table — higher number = tighter binding.
215
- const PREC: Record<string, number> = {
216
- '||': 1, '&&': 2,
217
- '=': 3, '!=': 3, '<>': 3, 'contains': 3,
218
- '<': 4, '>': 4, '<=': 4, '>=': 4,
219
- '+': 5, '-': 5,
220
- '*': 6, '/': 6,
221
- };
222
-
223
- function parseExpression(tokens: Token[], pos: number, ctx: EvalContext, minPrec = 1): [unknown, number] {
224
- let [lhs, next] = parseUnary(tokens, pos, ctx);
225
- while (next < tokens.length) {
226
- const t = tokens[next];
227
- if (t.kind !== 'op') break;
228
- const prec = PREC[t.value];
229
- if (prec === undefined || prec < minPrec) break;
230
- const op = t.value;
231
- const [rhs, after] = parseExpression(tokens, next + 1, ctx, prec + 1);
232
- lhs = applyBinary(op, lhs, rhs);
233
- next = after;
234
- }
235
- return [lhs, next];
236
- }
237
-
238
- function parseUnary(tokens: Token[], pos: number, ctx: EvalContext): [unknown, number] {
239
- const t = tokens[pos];
240
- if (!t) throw new Error('unexpected end of expression');
241
- if (t.kind === 'op' && t.value === '!') {
242
- const [inner, after] = parseUnary(tokens, pos + 1, ctx);
243
- return [!truthy(inner), after];
244
- }
245
- if (t.kind === 'op' && t.value === '-') {
246
- const [inner, after] = parseUnary(tokens, pos + 1, ctx);
247
- return [-Number(inner), after];
248
- }
249
- return parsePrimary(tokens, pos, ctx);
250
- }
251
-
252
- function parsePrimary(tokens: Token[], pos: number, ctx: EvalContext): [unknown, number] {
253
- const t = tokens[pos];
254
- if (!t) throw new Error('unexpected end of expression');
255
- switch (t.kind) {
256
- case 'num': return [t.value, pos + 1];
257
- case 'str': return [t.value, pos + 1];
258
- case 'bool': return [t.value, pos + 1];
259
- case 'null': return [null, pos + 1];
260
- case 'answer_ref': return [ctx.answers[t.name], pos + 1];
261
- case 'variable_ref': {
262
- // Resolution rules for {{name.suffix}}:
263
- // • {{q1.answer}} → ctx.answers.q1 (special-case for legacy)
264
- // • {{customer_name}} → ctx.variables.customer_name
265
- // • {{device.theme}} → walk dotted path into ctx.variables.device
266
- // • {{a.b.c.d}} → walk into ctx.variables.a.b.c.d
267
- // A dotted suffix that hits a non-object mid-walk returns undefined —
268
- // which then compares falsy in showWhen. This lets authors reference
269
- // any structured runtime signal (device, session, session.location, …)
270
- // without adding new token kinds — they just live under variables.*
271
- // as nested objects, injected by the SDK at render time.
272
- if (t.suffix === 'answer') return [ctx.answers[t.name], pos + 1];
273
- const root = ctx.variables[t.name];
274
- if (!t.suffix) return [root, pos + 1];
275
- let cursor: unknown = root;
276
- for (const seg of t.suffix.split('.')) {
277
- if (cursor == null || typeof cursor !== 'object') { cursor = undefined; break; }
278
- cursor = (cursor as Record<string, unknown>)[seg];
279
- }
280
- return [cursor, pos + 1];
281
- }
282
- case 'lparen': {
283
- const [inner, after] = parseExpression(tokens, pos + 1, ctx, 1);
284
- if (tokens[after]?.kind !== 'rparen') throw new Error('missing closing paren');
285
- return [inner, after + 1];
286
- }
287
- default: throw new Error(`unexpected token: ${JSON.stringify(t)}`);
288
- }
289
- }
290
-
291
- // ─── Operators ───────────────────────────────────────────────────────
292
-
293
- function applyBinary(op: string, lhs: unknown, rhs: unknown): unknown {
294
- switch (op) {
295
- case '=': return String(lhs) === String(rhs);
296
- case '!=':
297
- case '<>': return String(lhs) !== String(rhs);
298
- case '<': return Number(lhs) < Number(rhs);
299
- case '>': return Number(lhs) > Number(rhs);
300
- case '<=': return Number(lhs) <= Number(rhs);
301
- case '>=': return Number(lhs) >= Number(rhs);
302
- case '&&': return truthy(lhs) && truthy(rhs);
303
- case '||': return truthy(lhs) || truthy(rhs);
304
- case '+': return typeof lhs === 'string' || typeof rhs === 'string'
305
- ? String(lhs) + String(rhs)
306
- : Number(lhs) + Number(rhs);
307
- case '-': return Number(lhs) - Number(rhs);
308
- case '*': return Number(lhs) * Number(rhs);
309
- case '/': return Number(lhs) / Number(rhs);
310
- case 'contains':
311
- if (Array.isArray(lhs)) return lhs.includes(rhs);
312
- return String(lhs ?? '').includes(String(rhs));
313
- default: return undefined;
314
- }
315
- }
316
-
317
- function truthy(v: unknown): boolean {
318
- if (v === false || v === null || v === undefined || v === 0 || v === '') return false;
319
- if (Array.isArray(v)) return v.length > 0;
320
- return true;
321
- }
package/src/index.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * @cxpinsight/survey-spec
3
- *
4
- * Behaviour that must be identical for a respondent regardless of how they
5
- * arrived — a link, a site running the web SDK, or a mobile app. Three renderers
6
- * implemented this separately and drifted: the React tokenizer silently hid every
7
- * question whose condition used `<=`, `>=`, `==`, `<>`, `&&` or `||`, and the web
8
- * SDK could not resolve any variable namespace except `device`.
9
- *
10
- * tools/survey-conformance runs the same fixtures through every renderer and
11
- * fails on any disagreement.
12
- */
13
- export { evaluateExpression, evaluateCondition, isParseable } from './expressionEval';
14
- export { resolveVariable } from './resolveVariable';
15
- export type { AnswersMap, VariablesMap, EvalContext } from './types';
16
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
package/src/index.js DELETED
@@ -1,22 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveVariable = exports.isParseable = exports.evaluateCondition = exports.evaluateExpression = void 0;
4
- /**
5
- * @cxpinsight/survey-spec
6
- *
7
- * Behaviour that must be identical for a respondent regardless of how they
8
- * arrived — a link, a site running the web SDK, or a mobile app. Three renderers
9
- * implemented this separately and drifted: the React tokenizer silently hid every
10
- * question whose condition used `<=`, `>=`, `==`, `<>`, `&&` or `||`, and the web
11
- * SDK could not resolve any variable namespace except `device`.
12
- *
13
- * tools/survey-conformance runs the same fixtures through every renderer and
14
- * fails on any disagreement.
15
- */
16
- var expressionEval_1 = require("./expressionEval");
17
- Object.defineProperty(exports, "evaluateExpression", { enumerable: true, get: function () { return expressionEval_1.evaluateExpression; } });
18
- Object.defineProperty(exports, "evaluateCondition", { enumerable: true, get: function () { return expressionEval_1.evaluateCondition; } });
19
- Object.defineProperty(exports, "isParseable", { enumerable: true, get: function () { return expressionEval_1.isParseable; } });
20
- var resolveVariable_1 = require("./resolveVariable");
21
- Object.defineProperty(exports, "resolveVariable", { enumerable: true, get: function () { return resolveVariable_1.resolveVariable; } });
22
- //# sourceMappingURL=index.js.map
package/src/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,mDAAsF;AAA7E,oHAAA,kBAAkB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,6GAAA,WAAW,OAAA;AAC3D,qDAAoD;AAA3C,kHAAA,eAAe,OAAA"}
package/src/index.ts DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * @cxpinsight/survey-spec
3
- *
4
- * Behaviour that must be identical for a respondent regardless of how they
5
- * arrived — a link, a site running the web SDK, or a mobile app. Three renderers
6
- * implemented this separately and drifted: the React tokenizer silently hid every
7
- * question whose condition used `<=`, `>=`, `==`, `<>`, `&&` or `||`, and the web
8
- * SDK could not resolve any variable namespace except `device`.
9
- *
10
- * tools/survey-conformance runs the same fixtures through every renderer and
11
- * fails on any disagreement.
12
- */
13
- export { evaluateExpression, evaluateCondition, isParseable } from './expressionEval';
14
- export { resolveVariable } from './resolveVariable';
15
- export type { AnswersMap, VariablesMap, EvalContext } from './types';
@@ -1,19 +0,0 @@
1
- /**
2
- * One rule for resolving `{{name}}` and `{{name.path}}` against the variable bag.
3
- *
4
- * It exists because the same reference meant two different things depending on
5
- * where it appeared. Conditions walked nested namespaces; text interpolation
6
- * looked up a flat dotted key and, when that missed, returned the NAMESPACE
7
- * OBJECT — so a nested bag rendered
8
- *
9
- * "Hi {{user.name}}" → Hi {"plan":"pro","name":"Sam","email":"…"}
10
- *
11
- * putting every field of that namespace on screen in front of the respondent.
12
- * An author has no reason to expect two resolution rules for one syntax, so
13
- * there is now one, and both callers use it.
14
- *
15
- * Flat key first, then walk: a bag built the old way keeps resolving unchanged,
16
- * and a nested bag resolves properly.
17
- */
18
- export declare function resolveVariable(variables: Record<string, unknown>, name: string, suffix?: string): unknown;
19
- //# sourceMappingURL=resolveVariable.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolveVariable.d.ts","sourceRoot":"","sources":["resolveVariable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CA4BT"}
@@ -1,52 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveVariable = resolveVariable;
4
- /**
5
- * One rule for resolving `{{name}}` and `{{name.path}}` against the variable bag.
6
- *
7
- * It exists because the same reference meant two different things depending on
8
- * where it appeared. Conditions walked nested namespaces; text interpolation
9
- * looked up a flat dotted key and, when that missed, returned the NAMESPACE
10
- * OBJECT — so a nested bag rendered
11
- *
12
- * "Hi {{user.name}}" → Hi {"plan":"pro","name":"Sam","email":"…"}
13
- *
14
- * putting every field of that namespace on screen in front of the respondent.
15
- * An author has no reason to expect two resolution rules for one syntax, so
16
- * there is now one, and both callers use it.
17
- *
18
- * Flat key first, then walk: a bag built the old way keeps resolving unchanged,
19
- * and a nested bag resolves properly.
20
- */
21
- function resolveVariable(variables, name, suffix) {
22
- if (!variables)
23
- return undefined;
24
- const full = suffix ? `${name}.${suffix}` : name;
25
- let value;
26
- if (Object.prototype.hasOwnProperty.call(variables, full)) {
27
- // 1. Exact flat key — what bags built before namespaces contained.
28
- value = variables[full];
29
- }
30
- else {
31
- // 2. Walk the dotted path into nested namespaces.
32
- const segments = full.split('.');
33
- let cursor = variables;
34
- for (const seg of segments) {
35
- if (cursor == null || typeof cursor !== 'object') {
36
- cursor = undefined;
37
- break;
38
- }
39
- cursor = cursor[seg];
40
- }
41
- value = cursor;
42
- }
43
- // 3. A namespace is not a value. `{{user}}` with no path resolves to the whole
44
- // object, and stringifying it puts every field of that bag — plan, email,
45
- // internal ids — on screen in front of the respondent. Applies to BOTH
46
- // lookup paths: a flat bag can hold an object under a bare key too.
47
- // Arrays are values (a multi-select answer), so they pass.
48
- if (value !== null && typeof value === 'object' && !Array.isArray(value))
49
- return undefined;
50
- return value;
51
- }
52
- //# sourceMappingURL=resolveVariable.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolveVariable.js","sourceRoot":"","sources":["resolveVariable.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,yBACE,SAAkC,EAClC,IAAY,EACZ,MAAe;IAEf,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAEjC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAEjD,IAAI,KAAc,CAAC;IACnB,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;QAC1D,mEAAmE;QACnE,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,kDAAkD;QAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,GAAY,SAAS,CAAC;QAChC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAAC,MAAM,GAAG,SAAS,CAAC;gBAAC,MAAM;YAAC,CAAC;YAChF,MAAM,GAAI,MAAkC,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC;QACD,KAAK,GAAG,MAAM,CAAC;IACjB,CAAC;IAED,+EAA+E;IAC/E,6EAA6E;IAC7E,0EAA0E;IAC1E,uEAAuE;IACvE,8DAA8D;IAC9D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE3F,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -1,50 +0,0 @@
1
- /**
2
- * One rule for resolving `{{name}}` and `{{name.path}}` against the variable bag.
3
- *
4
- * It exists because the same reference meant two different things depending on
5
- * where it appeared. Conditions walked nested namespaces; text interpolation
6
- * looked up a flat dotted key and, when that missed, returned the NAMESPACE
7
- * OBJECT — so a nested bag rendered
8
- *
9
- * "Hi {{user.name}}" → Hi {"plan":"pro","name":"Sam","email":"…"}
10
- *
11
- * putting every field of that namespace on screen in front of the respondent.
12
- * An author has no reason to expect two resolution rules for one syntax, so
13
- * there is now one, and both callers use it.
14
- *
15
- * Flat key first, then walk: a bag built the old way keeps resolving unchanged,
16
- * and a nested bag resolves properly.
17
- */
18
- export function resolveVariable(
19
- variables: Record<string, unknown>,
20
- name: string,
21
- suffix?: string,
22
- ): unknown {
23
- if (!variables) return undefined;
24
-
25
- const full = suffix ? `${name}.${suffix}` : name;
26
-
27
- let value: unknown;
28
- if (Object.prototype.hasOwnProperty.call(variables, full)) {
29
- // 1. Exact flat key — what bags built before namespaces contained.
30
- value = variables[full];
31
- } else {
32
- // 2. Walk the dotted path into nested namespaces.
33
- const segments = full.split('.');
34
- let cursor: unknown = variables;
35
- for (const seg of segments) {
36
- if (cursor == null || typeof cursor !== 'object') { cursor = undefined; break; }
37
- cursor = (cursor as Record<string, unknown>)[seg];
38
- }
39
- value = cursor;
40
- }
41
-
42
- // 3. A namespace is not a value. `{{user}}` with no path resolves to the whole
43
- // object, and stringifying it puts every field of that bag — plan, email,
44
- // internal ids — on screen in front of the respondent. Applies to BOTH
45
- // lookup paths: a flat bag can hold an object under a bare key too.
46
- // Arrays are values (a multi-select answer), so they pass.
47
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) return undefined;
48
-
49
- return value;
50
- }
package/src/types.d.ts DELETED
@@ -1,17 +0,0 @@
1
- /** Answers keyed by question id, as the renderer has collected them so far. */
2
- export type AnswersMap = Record<string, unknown>;
3
- /**
4
- * The variable environment a condition resolves against.
5
- *
6
- * Namespaces are NESTED objects — `variables.device.theme`, `variables.user.plan`
7
- * — which is what lets an SDK expose structured runtime signals without the
8
- * expression grammar growing new token kinds. The web SDK used to flatten one
9
- * namespace by concatenating `'device.'` onto a key, which meant every OTHER
10
- * namespace silently evaluated false there and true elsewhere.
11
- */
12
- export type VariablesMap = Record<string, unknown>;
13
- export interface EvalContext {
14
- variables: VariablesMap;
15
- answers: AnswersMap;
16
- }
17
- //# sourceMappingURL=types.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,UAAU,CAAC;CACrB"}
package/src/types.js DELETED
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=types.js.map
package/src/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
package/src/types.ts DELETED
@@ -1,18 +0,0 @@
1
- /** Answers keyed by question id, as the renderer has collected them so far. */
2
- export type AnswersMap = Record<string, unknown>;
3
-
4
- /**
5
- * The variable environment a condition resolves against.
6
- *
7
- * Namespaces are NESTED objects — `variables.device.theme`, `variables.user.plan`
8
- * — which is what lets an SDK expose structured runtime signals without the
9
- * expression grammar growing new token kinds. The web SDK used to flatten one
10
- * namespace by concatenating `'device.'` onto a key, which meant every OTHER
11
- * namespace silently evaluated false there and true elsewhere.
12
- */
13
- export type VariablesMap = Record<string, unknown>;
14
-
15
- export interface EvalContext {
16
- variables: VariablesMap;
17
- answers: AnswersMap;
18
- }