@cxpinsight/survey-spec 0.5.1 → 0.7.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.
@@ -198,8 +198,10 @@ function tokenize(src) {
198
198
  tokens.push({ kind: 'op', value: '&&' });
199
199
  else if (lower === 'or')
200
200
  tokens.push({ kind: 'op', value: '||' });
201
+ // Kept DISTINCT from `!` on purpose — the two do not bind the same way.
202
+ // See parseUnary: the word binds below comparison, the symbol above it.
201
203
  else if (lower === 'not')
202
- tokens.push({ kind: 'op', value: '!' });
204
+ tokens.push({ kind: 'op', value: 'not' });
203
205
  else if (lower === 'contains')
204
206
  tokens.push({ kind: 'op', value: 'contains' });
205
207
  else
@@ -243,10 +245,36 @@ function parseExpression(tokens, pos, ctx, minPrec = 1) {
243
245
  }
244
246
  return [lhs, next];
245
247
  }
248
+ /**
249
+ * Precedence of the WORD `not`, which is deliberately not the precedence of `!`.
250
+ *
251
+ * `not {a} = 2` used to parse as `(not {a}) = 2` — false for every value of `a`,
252
+ * so an author writing "a is not 2" got a condition that could never be true and
253
+ * a question that never appeared. Conventional precedence, and SurveyJS whose
254
+ * dialect this emulates, put `not` BELOW comparison.
255
+ *
256
+ * It became urgent rather than latent when the server started shipping compiled
257
+ * trees: the compiler's `notExpr` already wrapped the whole comparison, so with
258
+ * a = 1 the same condition returned false here and true from the tree. One
259
+ * survey, two answers, decided by which path happened to run.
260
+ *
261
+ * 3 is the comparison tier in PREC, so `not` swallows a comparison and its
262
+ * arithmetic and stops at `and`/`or` — matching the compiler exactly:
263
+ * not {a} = 2 and {b} = 1 → (not ({a} = 2)) and ({b} = 1)
264
+ *
265
+ * `!` is left alone. The compiler rejects it outright, so no tree exists to
266
+ * disagree with, and it binds tightly in every C-family language including the
267
+ * JavaScript authors are borrowing from.
268
+ */
269
+ const NOT_WORD_PREC = 3;
246
270
  function parseUnary(tokens, pos, ctx) {
247
271
  const t = tokens[pos];
248
272
  if (!t)
249
273
  throw new Error('unexpected end of expression');
274
+ if (t.kind === 'op' && t.value === 'not') {
275
+ const [inner, after] = parseExpression(tokens, pos + 1, ctx, NOT_WORD_PREC);
276
+ return [!truthy(inner), after];
277
+ }
250
278
  if (t.kind === 'op' && t.value === '!') {
251
279
  const [inner, after] = parseUnary(tokens, pos + 1, ctx);
252
280
  return [!truthy(inner), after];
package/dist/index.d.ts CHANGED
@@ -15,8 +15,8 @@ export { evaluateTree, compareValues, MAX_TREE_DEPTH } from './evaluateTree';
15
15
  export type { ConditionNode, Operand, TreeContext } from './evaluateTree';
16
16
  export { resolveVariable } from './resolveVariable';
17
17
  export { splitIntoPages } from './pageFlow';
18
- export { validateAnswer, impliedChoices, impliedActions, impliedStepActions, impliedEqualProminence, safeActionHref, safeImageSrc, isDeferChoice, canSwipeToDismiss, autoDismissMs, deadlineCountdown, formatCountdown, outcomeFor, voiceUsable, rewardToShow, cardClickTarget, cardHoverEffect, resolveAction } from './validate';
19
- export type { ValidatableQuestion, OutcomeKind, OutcomeInput, ActionKind, RawAction, ResolvedAction } from './validate';
18
+ export { complianceGate, questionForValidation, validateAnswer, impliedChoices, impliedActions, impliedStepActions, impliedEqualProminence, safeActionHref, safeImageSrc, isDeferChoice, canSwipeToDismiss, autoDismissMs, deadlineCountdown, formatCountdown, outcomeFor, voiceUsable, rewardToShow, cardClickTarget, cardHoverEffect, resolveAction } from './validate';
19
+ export type { ComplianceState, EscapeHatch, ComplianceInput, ComplianceVerdict, ValidatableQuestion, OutcomeKind, OutcomeInput, ActionKind, RawAction, ResolvedAction } from './validate';
20
20
  export { validateAnswerCode } from './validate';
21
21
  export { wordMessage, EN_MESSAGES, MESSAGE_CODES } from './messages';
22
22
  export type { ValidationMessage } from './messages';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnU,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxH,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EACL,mBAAmB,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EACzE,kBAAkB,EAAE,SAAS,EAAE,iBAAiB,EAChD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EACjD,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EACtE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,GAC7D,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvF,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC1W,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC1L,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EACL,mBAAmB,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EACzE,kBAAkB,EAAE,SAAS,EAAE,iBAAiB,EAChD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EACjD,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EACtE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,GAC7D,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvF,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.optionLabelsOf = exports.voiceOptionsOf = exports.optionValueOf = exports.optionLabelOf = exports.shortCode = exports.buildPromptText = exports.scaffoldFor = exports.skipNoteText = exports.voiceDisclaimer = exports.auditVoiceQuestions = exports.voiceCanHandle = exports.voiceSupport = exports.distinctiveLabels = exports.matchDate = exports.matchNumberInRange = exports.rankOptions = exports.matchOptions = exports.matchOption = exports.matchBoolean = exports.normalizeTranscript = exports.MESSAGE_CODES = exports.EN_MESSAGES = exports.wordMessage = exports.validateAnswerCode = exports.resolveAction = exports.cardHoverEffect = exports.cardClickTarget = exports.rewardToShow = exports.voiceUsable = exports.outcomeFor = exports.formatCountdown = exports.deadlineCountdown = exports.autoDismissMs = exports.canSwipeToDismiss = exports.isDeferChoice = exports.safeImageSrc = exports.safeActionHref = exports.impliedEqualProminence = exports.impliedStepActions = exports.impliedActions = exports.impliedChoices = exports.validateAnswer = exports.splitIntoPages = exports.resolveVariable = exports.MAX_TREE_DEPTH = exports.compareValues = exports.evaluateTree = exports.isParseable = exports.evaluateCondition = exports.evaluateExpression = void 0;
3
+ exports.optionValueOf = exports.optionLabelOf = exports.shortCode = exports.buildPromptText = exports.scaffoldFor = exports.skipNoteText = exports.voiceDisclaimer = exports.auditVoiceQuestions = exports.voiceCanHandle = exports.voiceSupport = exports.distinctiveLabels = exports.matchDate = exports.matchNumberInRange = exports.rankOptions = exports.matchOptions = exports.matchOption = exports.matchBoolean = exports.normalizeTranscript = exports.MESSAGE_CODES = exports.EN_MESSAGES = exports.wordMessage = exports.validateAnswerCode = exports.resolveAction = exports.cardHoverEffect = exports.cardClickTarget = exports.rewardToShow = exports.voiceUsable = exports.outcomeFor = exports.formatCountdown = exports.deadlineCountdown = exports.autoDismissMs = exports.canSwipeToDismiss = exports.isDeferChoice = exports.safeImageSrc = exports.safeActionHref = exports.impliedEqualProminence = exports.impliedStepActions = exports.impliedActions = exports.impliedChoices = exports.validateAnswer = exports.questionForValidation = exports.complianceGate = exports.splitIntoPages = exports.resolveVariable = exports.MAX_TREE_DEPTH = exports.compareValues = exports.evaluateTree = exports.isParseable = exports.evaluateCondition = exports.evaluateExpression = void 0;
4
+ exports.optionLabelsOf = exports.voiceOptionsOf = void 0;
4
5
  /**
5
6
  * @cxpinsight/survey-spec
6
7
  *
@@ -26,6 +27,8 @@ Object.defineProperty(exports, "resolveVariable", { enumerable: true, get: funct
26
27
  var pageFlow_1 = require("./pageFlow");
27
28
  Object.defineProperty(exports, "splitIntoPages", { enumerable: true, get: function () { return pageFlow_1.splitIntoPages; } });
28
29
  var validate_1 = require("./validate");
30
+ Object.defineProperty(exports, "complianceGate", { enumerable: true, get: function () { return validate_1.complianceGate; } });
31
+ Object.defineProperty(exports, "questionForValidation", { enumerable: true, get: function () { return validate_1.questionForValidation; } });
29
32
  Object.defineProperty(exports, "validateAnswer", { enumerable: true, get: function () { return validate_1.validateAnswer; } });
30
33
  Object.defineProperty(exports, "impliedChoices", { enumerable: true, get: function () { return validate_1.impliedChoices; } });
31
34
  Object.defineProperty(exports, "impliedActions", { enumerable: true, get: function () { return validate_1.impliedActions; } });
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,4EAA4E;AAC5E,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CA4B9E,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAI5E;AAED,0EAA0E;AAC1E,eAAO,MAAM,aAAa,UAA2B,CAAC"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,4EAA4E;AAC5E,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAiC9E,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAI5E;AAED,0EAA0E;AAC1E,eAAO,MAAM,aAAa,UAA2B,CAAC"}
package/dist/messages.js CHANGED
@@ -25,6 +25,11 @@ exports.wordMessage = wordMessage;
25
25
  /** English wording, one entry per code. The only prose in the validator. */
26
26
  exports.EN_MESSAGES = {
27
27
  required: () => 'This field is required',
28
+ // Compliance. Written for someone who is being held, not for a developer:
29
+ // say what is happening and what happens next, never just "blocked".
30
+ compliance_under_review: () => 'Thanks — someone is reviewing this now. '
31
+ + 'You do not need to do anything else, and this will update when the review is done.',
32
+ compliance_blocked: () => 'This needs to be accepted before you can continue.',
28
33
  choose_option: () => 'Please choose an option',
29
34
  format_email: () => 'Please enter a valid email address (e.g., you@example.com)',
30
35
  format_url: () => 'Please enter a full URL, including https://',
@@ -8,6 +8,8 @@ export interface ValidatableQuestion {
8
8
  maxLength?: number;
9
9
  minValue?: number;
10
10
  maxValue?: number;
11
+ /** spec §8's other spelling of a pattern rule — see validateRules. */
12
+ regex?: string;
11
13
  minCount?: number;
12
14
  maxCount?: number;
13
15
  rules?: unknown;
@@ -257,6 +259,101 @@ export interface OutcomeInput {
257
259
  * surface you get past by saying no is not a compliance surface.
258
260
  */
259
261
  export declare function outcomeFor(input: OutcomeInput): OutcomeKind;
262
+ /**
263
+ * The question as it should be validated RIGHT NOW.
264
+ *
265
+ * `requiredWhen` — "required only if they said yes earlier" — was compiled by
266
+ * the server and read by no renderer at all. An author who set it got a question
267
+ * that was never required, on every channel, with nothing reporting it. The last
268
+ * of the four conditions to be inert everywhere rather than somewhere.
269
+ *
270
+ * ADDITIVE, deliberately: a question already marked required stays required. The
271
+ * alternative reading — that requiredWhen REPLACES required — would let a
272
+ * condition make an existing question optional, so a survey that collects an
273
+ * answer today could quietly stop collecting it. This direction can only ever
274
+ * ask for more, never less, which is the safe way to turn on a rule that has
275
+ * never run.
276
+ *
277
+ * The caller supplies the evaluator because each renderer has its own, already
278
+ * wired to its answers and its compiled trees. What lives here is the decision,
279
+ * which is the part that must not differ between them.
280
+ *
281
+ * @param evaluate must FAIL OPEN by returning false for a condition it cannot
282
+ * read — an unreadable requiredWhen leaves the question optional rather than
283
+ * blocking a respondent behind a rule nobody can see.
284
+ */
285
+ export declare function questionForValidation<T extends {
286
+ required?: unknown;
287
+ isRequired?: unknown;
288
+ requiredWhen?: unknown;
289
+ }>(question: T, evaluate: (expr: string) => boolean): T;
290
+ /** Where a compliance interaction has got to. */
291
+ export type ComplianceState = 'open' | 'blocked' | 'under_review' | 'cleared';
292
+ /** What a respondent is offered when they cannot simply proceed. */
293
+ export type EscapeHatch = 'none' | 'contact' | 'defer';
294
+ export interface ComplianceInput {
295
+ interactionType?: string | null;
296
+ /** The implied choice taken, or null before they have answered. */
297
+ action?: string | null;
298
+ /** True when the chosen action is the affirmative one. */
299
+ affirmative?: boolean;
300
+ compliance?: {
301
+ reviewRequired?: unknown;
302
+ onDecline?: unknown;
303
+ blockedMessage?: unknown;
304
+ reviewMessage?: unknown;
305
+ escapeHatch?: unknown;
306
+ contactHref?: unknown;
307
+ } | null;
308
+ }
309
+ export interface ComplianceVerdict {
310
+ state: ComplianceState;
311
+ /** May the respondent close the surface? */
312
+ dismissible: boolean;
313
+ escape: EscapeHatch;
314
+ /** Where `escape: 'contact'` sends them. Null when there is nowhere. */
315
+ contactHref: string | null;
316
+ /** A wording code, or null when there is nothing to say. */
317
+ messageCode: 'compliance_blocked' | 'compliance_under_review' | null;
318
+ /** The author's own prose, which travels verbatim and is never translated. */
319
+ authorMessage: string | null;
320
+ /** True when this state is worth recording — see the monitoring note. */
321
+ reportable: boolean;
322
+ }
323
+ /**
324
+ * Decide whether a compliance interaction lets the respondent go.
325
+ *
326
+ * This is the half of compliance that was never built. `outcomeFor` already
327
+ * returns `hold`, meaning "do not let them through yet", and all three renderers
328
+ * read it — to choose a closing message. Nothing anywhere connected it to
329
+ * whether the surface could be CLOSED, so a compliance check was dismissed
330
+ * exactly like a rating prompt. A compliance surface you get past by ignoring it
331
+ * is not a compliance surface.
332
+ *
333
+ * Three states hold, and they are not the same thing to the person looking at
334
+ * them:
335
+ *
336
+ * open they have not answered yet
337
+ * blocked they declined, and the author chose to hold them
338
+ * under_review they accepted, and a person has to look at it
339
+ *
340
+ * `under_review` matters most and is the easiest to get wrong. Letting someone
341
+ * through on submit tells them they are finished when they are not, and defeats
342
+ * the check; saying nothing leaves them staring at a surface that looks stuck.
343
+ * So it holds AND it says so.
344
+ *
345
+ * ── The escape hatch ──
346
+ * A surface that cannot be closed can trap someone whose answer will never be
347
+ * accepted — a failed document check at 2am with nobody to ask. The author
348
+ * chooses which way out exists, and the default is `contact` rather than `none`,
349
+ * because a route to a human costs the check nothing while a dead end costs a
350
+ * customer everything. `defer` is the only one that actually releases them; it
351
+ * is a postponement, and the interaction comes back.
352
+ *
353
+ * Anything unrecognised falls back to `contact` rather than to `none`: a
354
+ * mis-typed setting must not silently become a trap.
355
+ */
356
+ export declare function complianceGate(input: ComplianceInput): ComplianceVerdict;
260
357
  /**
261
358
  * Can voice mode do anything on this step?
262
359
  *
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
package/dist/validate.js CHANGED
@@ -14,6 +14,8 @@ exports.autoDismissMs = autoDismissMs;
14
14
  exports.deadlineCountdown = deadlineCountdown;
15
15
  exports.formatCountdown = formatCountdown;
16
16
  exports.outcomeFor = outcomeFor;
17
+ exports.questionForValidation = questionForValidation;
18
+ exports.complianceGate = complianceGate;
17
19
  exports.voiceUsable = voiceUsable;
18
20
  exports.rewardToShow = rewardToShow;
19
21
  exports.cardClickTarget = cardClickTarget;
@@ -194,6 +196,34 @@ function validateTextish(question, value) {
194
196
  if (typeof question.minLength === 'number' && trimmed.length < question.minLength) {
195
197
  return { code: 'min_length', params: { min: question.minLength } };
196
198
  }
199
+ // minValue / maxValue on a NUMERIC text input.
200
+ //
201
+ // These used to do nothing here. validateNumericRange is reached only from
202
+ // rating/csat/ces/nps, which carry real numbers; a question with
203
+ // inputType 'number' submits the STRING '5' and arrived at this function
204
+ // instead, which checked lengths and formats and never a range. An author
205
+ // setting "between 1 and 10" on a number question got a control that read as
206
+ // configured and enforced nothing, on every channel at once.
207
+ //
208
+ // The coercion is not new behaviour invented here: applyRule already does
209
+ // exactly this for a rules[] entry of type min/max. So the same author intent
210
+ // was enforced when written one way and ignored when written the other. This
211
+ // makes the two agree.
212
+ //
213
+ // Guarded on inputType rather than applied to all text, because a range on a
214
+ // free-text comment is meaningless and coercing it would reject prose for
215
+ // being NaN.
216
+ if (inputType === 'number' && trimmed !== '') {
217
+ const n = Number(trimmed);
218
+ if (!Number.isNaN(n)) {
219
+ if (typeof question.minValue === 'number' && n < question.minValue) {
220
+ return { code: 'min_value', params: { min: question.minValue } };
221
+ }
222
+ if (typeof question.maxValue === 'number' && n > question.maxValue) {
223
+ return { code: 'max_value', params: { max: question.maxValue } };
224
+ }
225
+ }
226
+ }
197
227
  return null;
198
228
  }
199
229
  function validateCheckbox(question, value) {
@@ -224,7 +254,20 @@ function validateNumericRange(question, value) {
224
254
  return null;
225
255
  }
226
256
  function validateRules(question, value) {
227
- const rules = Array.isArray(question.rules) ? question.rules : [];
257
+ const declared = Array.isArray(question.rules) ? question.rules : [];
258
+ // A top-level `regex` is the other spelling of a pattern rule, and it was read
259
+ // by nothing: `rules: [{ type: 'pattern' }]` rejected a bad value while
260
+ // `regex: '…'` accepted it, from the same author meaning the same thing. The
261
+ // setting did not fail — it disappeared, on every channel, with no warning.
262
+ //
263
+ // Treated as a pattern rule so one intent has one behaviour. This is the same
264
+ // client-side exposure the existing spelling already carries: a catastrophic
265
+ // pattern costs the respondent their own tab, which is why the SERVER still
266
+ // refuses to evaluate either spelling — there the cost is shared, and
267
+ // `^(a+)+$` against 24 characters takes 116ms and doubles every two.
268
+ const rules = typeof question.regex === 'string' && question.regex !== ''
269
+ ? declared.concat([{ type: 'pattern', value: question.regex }])
270
+ : declared;
228
271
  if (rules.length === 0)
229
272
  return null;
230
273
  const str = typeof value === 'string' ? value : String(value ?? '');
@@ -697,6 +740,126 @@ function outcomeFor(input) {
697
740
  // for something we do not recognise: it acknowledges without promising.
698
741
  return 'thanks';
699
742
  }
743
+ /**
744
+ * The question as it should be validated RIGHT NOW.
745
+ *
746
+ * `requiredWhen` — "required only if they said yes earlier" — was compiled by
747
+ * the server and read by no renderer at all. An author who set it got a question
748
+ * that was never required, on every channel, with nothing reporting it. The last
749
+ * of the four conditions to be inert everywhere rather than somewhere.
750
+ *
751
+ * ADDITIVE, deliberately: a question already marked required stays required. The
752
+ * alternative reading — that requiredWhen REPLACES required — would let a
753
+ * condition make an existing question optional, so a survey that collects an
754
+ * answer today could quietly stop collecting it. This direction can only ever
755
+ * ask for more, never less, which is the safe way to turn on a rule that has
756
+ * never run.
757
+ *
758
+ * The caller supplies the evaluator because each renderer has its own, already
759
+ * wired to its answers and its compiled trees. What lives here is the decision,
760
+ * which is the part that must not differ between them.
761
+ *
762
+ * @param evaluate must FAIL OPEN by returning false for a condition it cannot
763
+ * read — an unreadable requiredWhen leaves the question optional rather than
764
+ * blocking a respondent behind a rule nobody can see.
765
+ */
766
+ function questionForValidation(question, evaluate) {
767
+ if (!question)
768
+ return question;
769
+ if (question.required === true || question.isRequired === true)
770
+ return question;
771
+ const expr = question.requiredWhen;
772
+ if (typeof expr !== 'string' || !expr.trim())
773
+ return question;
774
+ let now = false;
775
+ try {
776
+ now = evaluate(expr) === true;
777
+ }
778
+ catch {
779
+ now = false;
780
+ }
781
+ return now ? { ...question, required: true } : question;
782
+ }
783
+ const ESCAPES = ['none', 'contact', 'defer'];
784
+ /**
785
+ * Decide whether a compliance interaction lets the respondent go.
786
+ *
787
+ * This is the half of compliance that was never built. `outcomeFor` already
788
+ * returns `hold`, meaning "do not let them through yet", and all three renderers
789
+ * read it — to choose a closing message. Nothing anywhere connected it to
790
+ * whether the surface could be CLOSED, so a compliance check was dismissed
791
+ * exactly like a rating prompt. A compliance surface you get past by ignoring it
792
+ * is not a compliance surface.
793
+ *
794
+ * Three states hold, and they are not the same thing to the person looking at
795
+ * them:
796
+ *
797
+ * open they have not answered yet
798
+ * blocked they declined, and the author chose to hold them
799
+ * under_review they accepted, and a person has to look at it
800
+ *
801
+ * `under_review` matters most and is the easiest to get wrong. Letting someone
802
+ * through on submit tells them they are finished when they are not, and defeats
803
+ * the check; saying nothing leaves them staring at a surface that looks stuck.
804
+ * So it holds AND it says so.
805
+ *
806
+ * ── The escape hatch ──
807
+ * A surface that cannot be closed can trap someone whose answer will never be
808
+ * accepted — a failed document check at 2am with nobody to ask. The author
809
+ * chooses which way out exists, and the default is `contact` rather than `none`,
810
+ * because a route to a human costs the check nothing while a dead end costs a
811
+ * customer everything. `defer` is the only one that actually releases them; it
812
+ * is a postponement, and the interaction comes back.
813
+ *
814
+ * Anything unrecognised falls back to `contact` rather than to `none`: a
815
+ * mis-typed setting must not silently become a trap.
816
+ */
817
+ function complianceGate(input) {
818
+ const free = {
819
+ state: 'cleared', dismissible: true, escape: 'none',
820
+ contactHref: null, messageCode: null, authorMessage: null, reportable: false,
821
+ };
822
+ if (String(input?.interactionType || '').toLowerCase() !== 'comply')
823
+ return free;
824
+ const c = input.compliance || {};
825
+ const raw = String(c.escapeHatch ?? '').toLowerCase();
826
+ const escape = ESCAPES.includes(raw) ? raw : 'contact';
827
+ // Through safeActionHref, not raw: this is a destination the host page is told
828
+ // to follow, so `javascript:` and friends are refused the same way an action
829
+ // button's destination is. `mailto:` passes, which is the common case for a
830
+ // route to a human.
831
+ const contactHref = (escape === 'contact' ? safeActionHref({ type: 'url', value: c.contactHref }) : null) ?? null;
832
+ const authorPose = (v) => (typeof v === 'string' && v.trim() ? v : null);
833
+ // `defer` is the only hatch that releases them — closing IS the postponement.
834
+ const canLeave = escape === 'defer';
835
+ const answered = input.action !== null && input.action !== undefined && input.action !== '';
836
+ if (!answered) {
837
+ return {
838
+ state: 'open', dismissible: canLeave, escape, contactHref,
839
+ messageCode: null, authorMessage: null, reportable: false,
840
+ };
841
+ }
842
+ if (input.affirmative === false) {
843
+ // A decline the author chose to accept is simply the end of it.
844
+ if (String(c.onDecline || 'block') !== 'block')
845
+ return free;
846
+ return {
847
+ state: 'blocked', dismissible: canLeave, escape, contactHref,
848
+ messageCode: 'compliance_blocked',
849
+ authorMessage: authorPose(c.blockedMessage),
850
+ reportable: true,
851
+ };
852
+ }
853
+ if (c.reviewRequired === true) {
854
+ return {
855
+ state: 'under_review', dismissible: canLeave, escape, contactHref,
856
+ messageCode: 'compliance_under_review',
857
+ authorMessage: authorPose(c.reviewMessage),
858
+ reportable: true,
859
+ };
860
+ }
861
+ return free;
862
+ }
700
863
  /**
701
864
  * Can voice mode do anything on this step?
702
865
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cxpinsight/survey-spec",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
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",