jsfe 1.12.0 → 1.13.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.
@@ -228,6 +228,7 @@ export function validateFlowContracts(flows: any, tools?: any[], { externalFlows
228
228
  max: number;
229
229
  };
230
230
  pii: boolean;
231
+ validator: string;
231
232
  question: string;
232
233
  question_es: string;
233
234
  };
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EC"}
@@ -365,6 +365,8 @@ export namespace FIELD_PRESETS {
365
365
  export { digits_6 as digits };
366
366
  let pii_2: boolean;
367
367
  export { pii_2 as pii };
368
+ let validator_2: string;
369
+ export { validator_2 as validator };
368
370
  let question_11: string;
369
371
  export { question_11 as question };
370
372
  let question_es_11: string;
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA4HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAgcD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3nBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmzB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAocD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAhoBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyzB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -82,6 +82,7 @@ export const FIELD_PRESETS = {
82
82
  retry: 'Please say or enter the expiration as four digits, month then year, like 0 9 2 8.', retry_es: 'Por favor diga o ingrese el vencimiento en cuatro dígitos, mes y año, por ejemplo 0 9 2 8.',
83
83
  retry_text: 'Please type the expiration as four digits, month then year, like 0928.', retry_text_es: 'Por favor escriba el vencimiento en cuatro dígitos, mes y año, por ejemplo 0928.' },
84
84
  card_cvv: { label: 'security code', label_es: 'código de seguridad', profile: { kind: 'digits', min: 3, max: 4 }, digits: { min: 3, max: 4 }, pii: true,
85
+ validator: 'validate-card-cvv',
85
86
  question: 'What is the security code on the back of the card?', question_es: '¿Cuál es el código de seguridad al reverso de la tarjeta?' },
86
87
  card_zip: { label: 'billing ZIP code', label_es: 'código postal de facturación', profile: { kind: 'digits', min: 5, max: 5 }, digits: { min: 5, max: 5 },
87
88
  question: 'What is the billing ZIP code for the card?', question_es: '¿Cuál es el código postal de facturación de la tarjeta?' },
@@ -602,6 +603,10 @@ const validators = [
602
603
  description: 'Validator: month and year, a real month, not expired (validateExpiration). Normalizes to MMYY. Params: input, value.',
603
604
  parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }],
604
605
  steps: [SIDE('vce_check', "(function (v) { var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')) || /^(0[1-9])0(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
606
+ { id: 'validate-card-cvv', name: 'ValidateCardCvv', version: '1.0.0', scope: 'isolated',
607
+ description: 'Validator: the security code has the length of its card brand, 4 digits for American Express (34, 37) and 3 for every other brand; with no card number yet, 3 or 4. Params: input, value, values.',
608
+ parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }, { name: 'values', type: 'object', description: "the form's values, for the card number" }],
609
+ steps: [SIDE('vcc_check', "(function (v, n) { var d = String(v).replace(/\\D/g, ''); var pan = String(n || '').replace(/\\D/g, ''); var want = pan ? (/^3[47]/.test(pan) ? 4 : 3) : 0; var ok = want ? d.length === want : d.length === 3 || d.length === 4; cargo.inputCheck = ok ? { ok: true, value: d } : want === 4 ? { ok: false, message: \"That security code doesn't look right. It is the 4 digits on the front of the card.\", message_es: 'Ese código de seguridad no parece correcto. Son los 4 dígitos al frente de la tarjeta.' } : { ok: false, message: \"That security code doesn't look right. It is the 3 digits on the back of the card.\", message_es: 'Ese código de seguridad no parece correcto. Son los 3 dígitos al reverso de la tarjeta.' }; return true; })(value, typeof values !== 'undefined' && values ? values.card_number : '')")] },
605
610
  ];
606
611
  // ── field collectors and sub-forms ──────────────────────────────────────────────────────────────────────────────────
607
612
  const fieldCollectors = Object.entries(FIELD_COLLECTORS).map(([id, field]) => formFlow({
@@ -857,6 +862,7 @@ const LIBRARY_CONTRACTS = {
857
862
  'load-source': { role: 'worker' }, 'answer-facts': { role: 'worker' },
858
863
  'validate-card-number': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
859
864
  'validate-card-expiration': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
865
+ 'validate-card-cvv': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
860
866
  'collect-phone': { role: 'collector' }, 'collect-email': { role: 'collector' }, 'collect-zip': { role: 'collector' },
861
867
  'collect-street-address': { role: 'collector' }, 'collect-city': { role: 'collector' }, 'collect-card-number': { role: 'collector' },
862
868
  'collect-card-expiration': { role: 'collector' }, 'collect-card-cvv': { role: 'collector' }, 'collect-card-zip': { role: 'collector' },
@@ -347,8 +347,8 @@ export function numericAmountReading(input) {
347
347
  // Mixed answers with an explicit monetary marker or payment verb still parse their independent amount.
348
348
  if (/\b(?:cards?|tarjetas?|ending|termina(?:d[ao]s?)?|visa|mastercard|amex|discover)\b/i.test(raw) && !isAmountAnswer(raw) && !/\b(?:pay|paying|pagar|abonar)(?:\b|(?=\d))/i.test(raw))
349
349
  return { value: NaN, candidates: 0 };
350
- // 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50
351
- let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/(\d),(\d{1,2})\b/g, '$1.$2');
350
+ // 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50; 1.500 → 1500 (three digits after a dot are never cents)
351
+ let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/\b\d{1,3}(?:\.\d{3})+\b(?![.,]\d)/g, (m) => m.replace(/\./g, '')).replace(/(\d),(\d{1,2})\b/g, '$1.$2');
352
352
  const dc = t.match(DOLLARS_AND_CENTS);
353
353
  if (dc) {
354
354
  t = t.replace(DOLLARS_AND_CENTS, () => ` $${dc[1]}.${dc[2].padStart(2, '0')} `);
@@ -1 +1 @@
1
- {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA0CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AA7oBM,8CAAmE"}
1
+ {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA6CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AAnpBM,8CAAmE"}
@@ -70,10 +70,13 @@ export function resolveFormInputs(inputs, presets = {}) {
70
70
  validateFormActivation(input, resolved);
71
71
  return resolved;
72
72
  }
73
- /** Dormant entry is explicit: named descendant fields may propose local controllers, never ambient state. */
73
+ /** Dormant entry is explicit: named descendant fields may propose local controllers, never ambient state. A subform
74
+ * names its entry fields (activate_on); a plain input is its own entry ("NEW CARD, LINK, or a different amount": an
75
+ * amount said there chooses the amount option), so it declares no activate_on. */
74
76
  function validateFormActivation(input, specs) {
75
- if (!input.form || !input.activate || typeof input.activate !== 'object' || Array.isArray(input.activate) || !Object.keys(input.activate).length || !Array.isArray(input.activate_on) || !input.activate_on.length)
76
- throw new Error(`FORM activation ${input.name} needs a subform, controllers and activate_on fields`);
77
+ const plain = !input.form && !!input.profile && input.activate_on === undefined;
78
+ if (!input.activate || typeof input.activate !== 'object' || Array.isArray(input.activate) || !Object.keys(input.activate).length || !plain && (!input.form || !Array.isArray(input.activate_on) || !input.activate_on.length))
79
+ throw new Error(`FORM activation ${input.name} needs controllers, and a subform with activate_on fields or a plain input without them`);
77
80
  for (const [name, value] of Object.entries(input.activate)) {
78
81
  const controller = specs.flatMap(s => s.any_of || [s]).find(s => s.name === name);
79
82
  if (!controller?.profile || controller.form || controller.bind || name === input.name || !hasFormValue(value) || typeof value === 'object')
@@ -82,7 +85,7 @@ function validateFormActivation(input, specs) {
82
85
  if (!reading || !own(reading, 'value') || !hasFormValue(reading.value) || !formValuesEqual(reading.value, value))
83
86
  throw new Error(`FORM activation ${input.name} has invalid controller value ${name}`);
84
87
  }
85
- for (const path of input.activate_on) {
88
+ for (const path of input.activate_on || []) {
86
89
  if (typeof path !== 'string' || !path || path.split('.').some(name => !name || forbidden.has(name)))
87
90
  throw new Error(`FORM activation ${input.name} has invalid entry field`);
88
91
  if (input.schema) {
@@ -188,11 +191,14 @@ export function formInputChain(forms, id, cargo = {}) {
188
191
  for (const input of spec.any_of || [spec]) {
189
192
  if (entries && !entries.includes(input.name))
190
193
  continue;
191
- if (formCondition(input, state.values, cargo, state) !== false && !input.form && input.profile)
194
+ const inputCondition = formCondition(input, state.values, cargo, state);
195
+ // A dormant plain input that declares activate proposes its controllers when it is answered.
196
+ const inputDormant = inputCondition !== true && !activation && !input.form && input.activate !== undefined ? dormantActivation(input, state, cargo) : null;
197
+ if ((inputCondition !== false || inputDormant) && !input.form && input.profile)
192
198
  out.push({ ...input,
193
199
  ...(prefix ? { label: `${prefix}: ${input.label || input.name}`, label_es: `${prefixEs || prefix}: ${input.label_es || input.label || input.name}` } : {}),
194
200
  ...(input.choices_from && Array.isArray(state.data?.[input.choices_from]) ? { profile: { ...input.profile, choices: state.data[input.choices_from] } } : {}),
195
- ...(activation ? { activation } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
201
+ ...(activation || inputDormant ? { activation: activation || inputDormant } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
196
202
  // its declared prerequisites hold accepted values: a field that cannot be validated yet is not a context for
197
203
  // an unnamed out-of-order answer (contract, reference §5)
198
204
  ready: (input.depends_on || spec.depends_on || []).every(name => hasFormValue(state.values[name])) });
@@ -211,7 +217,7 @@ export function enrichFormMatches(matches, chain, form, names = [], asked = '')
211
217
  const enrich = h => {
212
218
  const spec = chain.find(c => c.form === h.form && c.name === h.name) || {};
213
219
  return { ...h, label: h.option_label || spec.label || h.name, label_es: h.option_label_es || spec.label_es || spec.label || h.name, ...(h.option_label ? { option: true } : {}),
214
- validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
220
+ validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), ...(spec.silent === true ? { silent: true } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
215
221
  };
216
222
  return { ...matches, hits: matches.hits.map(enrich), ambiguous: matches.ambiguous.map(enrich), remaining: (matches.remaining || []).map(group => group.map(enrich)) };
217
223
  }
@@ -1 +1 @@
1
- {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoLC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoMC"}
@@ -299,6 +299,9 @@ export function matchInputs(answer, candidates, current, choiceKey) {
299
299
  const spanText = (reading) => (reading.spans || []).map(([a, b]) => raw.slice(a, b)).join(' ');
300
300
  const refused = [];
301
301
  let readings = [];
302
+ // An invited input is read as in order while it is asked (docs/copilot-reference.md §5): its asked signature, not
303
+ // its narrower out-of-order one ("40" at "NEW CARD, LINK, or a different amount" is the amount).
304
+ const invitedInput = new Set([...list.filter(isCur).flatMap((c) => Array.isArray(c.invites) ? c.invites : []), ...(Array.isArray(cur.invites) ? cur.invites : [])]);
302
305
  for (const c of list) {
303
306
  // Rule 2: a confirmation is answered only when asked, firm by CONFIRM_WORDS, soft by the wider YES vocabulary.
304
307
  const confirmation = ['firm', 'soft'].includes(c.confirmation) ? c.confirmation : '';
@@ -313,7 +316,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
313
316
  }
314
317
  const vocabulary = confirmation ? { ...c.profile, choices: (c.profile.choices || []).map((x) => x.key === 'yes' ? { ...x, phrases: [...(x.phrases ?? x.words ?? []), ...(confirmation === 'firm' ? CONFIRM_WORDS : ANSWER_WORDS.yes)], exact: confirmation === 'firm' || x.exact === true } : x.key === 'no' ? { ...x, phrases: [...(x.phrases ?? x.words ?? []), ...ANSWER_WORDS.no] } : x) } : c.profile;
315
318
  const profile = narrativeCurrent && vocabulary.kind === 'email' ? { ...vocabulary, literal: true } : vocabulary;
316
- let r = readInputProfile(profile, raw, isCur(c));
319
+ let r = readInputProfile(profile, raw, isCur(c) || invitedInput.has(c.name) && c.confirmation === undefined);
317
320
  // A semantic choice supplied by ask-choice (e.g. spoken keypad keys) is a reading, not an early return.
318
321
  if (!r && c === own && choiceKey && c.profile.kind === 'choice' && (c.profile.choices || []).some(x => x.key === choiceKey)) {
319
322
  r = { ...(r || {}), value: choiceKey, spans: r?.spans?.length ? r.spans : [[0, raw.length]] };
@@ -346,6 +349,20 @@ export function matchInputs(answer, candidates, current, choiceKey) {
346
349
  const wholeRuns = (reading) => runs.every(([a, b]) => !(reading.spans || []).some(([s, e]) => s < b && a < e) || (reading.spans || []).some(([s, e]) => s <= a && e >= b));
347
350
  // Rule 4: a bare number has no money mark and no word in its evidence.
348
351
  const bare = (reading) => !isAmountAnswer(raw) && (reading.spans || []).length > 0 && (reading.spans || []).every(([a, b]) => /\d/.test(raw.slice(a, b)) && !/\p{L}/u.test(raw.slice(a, b)));
352
+ // Rule 1 by shape: a bare number whose length is the current question's own keypad length (`digits`) answers that
353
+ // question even when its reader rejects it (an expiration "1320" is month 13); its validation decides and the
354
+ // question is asked again. It is never read out of order as another field (that "1320" is not a security code).
355
+ // A choice is excluded (a digit it does not offer reaches the invited amount, rule 4), and so is an amount.
356
+ const bareDigits = raw.replace(/\D/g, '');
357
+ const shapeOf = own && own.digits && !['choice', 'amount', 'text'].includes(own.profile.kind) ? own.digits : null;
358
+ if (shapeOf && bareDigits && /^[\d\s./-]+$/.test(raw.trim()) && !isAmountAnswer(raw) && bareDigits.length >= Number(shapeOf.min || 1) && bareDigits.length <= Number(shapeOf.max || shapeOf.min || 1)
359
+ && !readings.some((reading) => reading.current || invited(reading))) {
360
+ readings.push({ form: own.form, name: own.name, value: bareDigits, ...(own.validator ? {} : { invalid: true }), known: false, repeatedChoice: false, wholeChoice: false, choice: false, narrative: false, resetOn: [], spans: [[0, raw.length]], keypad: false, fallback: false,
361
+ group: own.group, exclusive: own.exclusive === true, level: forms.indexOf(own.scope || own.form), current: true, confirm: Array.isArray(own.confirm_against) ? own.confirm_against : [] });
362
+ }
363
+ else {
364
+ // not a bare number of the current question's shape, or the current question read it
365
+ }
349
366
  readings = readings.filter((reading) => {
350
367
  if (inOrder(reading)) {
351
368
  if (!reading.current)
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:d2fe25c463500cc6a524418c9ccb5ac710130ac6cce764894ca522f6fe83a22c";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:d1d531f3fad961b1d17d88b5fb63fffff2d5ddfaf837882fe0345bc496ce5dc2";
@@ -149,7 +149,7 @@ Use one of these input shapes:
149
149
 
150
150
  | Shape | Fields |
151
151
  | --- | --- |
152
- | Preset field | `name`, `field`; optional overrides. Presets: `phone`, `email`, `zip`, `account_number`, `ssn_last4`, `amount`, `street_address`, `city`, `state`, `card_number`, `card_expiration`, `card_cvv`, `card_zip`. |
152
+ | Preset field | `name`, `field`; optional overrides. Presets: `phone`, `email`, `zip`, `account_number`, `ssn_last4`, `amount`, `street_address`, `city`, `state`, `card_number`, `card_expiration`, `card_cvv`, `card_zip`. `card_cvv` takes its card brand's length (`validate-card-cvv`: 4 digits for American Express, 3 for every other brand). |
153
153
  | Inline field | `name`, `profile`, questions and business validation. |
154
154
  | Child form | `name`, `form:"installed-child-id"`, optional `parameters`. JSFE links its schema; do not copy the derived `schema` into editable source. |
155
155
  | Alternative group | `name`, `any_of:[leaf-or-child declarations]`; optional `exclusive:true`. At least one applicable member satisfies the group; exclusive competing answers require clarification. |
@@ -172,7 +172,8 @@ Field options:
172
172
  | `choices_from`, `question_from` | Read prepared choices/question from the owner's `data`; corresponding localized question storage is handled by shared workers. |
173
173
  | `digits`, `pii`, `as_spoken` | Collection hints. FORM preserves original speech by default; false explicitly opts into cleanup. |
174
174
  | `confirm_against` | Declare overlapping field interpretations that require clarification even when normal current-field priority applies. |
175
- | `activate`, `activate_on` | Narrow dormant-child activation: proposed controller assignments plus nominated descendant entry paths. Controller and entry validate atomically; a rejected entry must not flip a controller. |
175
+ | `activate`, `activate_on` | Narrow dormant activation: proposed controller assignments plus, for a subform, nominated descendant entry paths (`activate_on`). A dormant plain input declares `activate` alone and is its own entry: an amount said at "NEW CARD, LINK, or a different amount" chooses the amount option (`revised_amount` with `when:{next:'amount'}`, `activate:{next:'amount'}`). Controller and entry validate atomically; a rejected entry must not flip a controller. |
176
+ | `silent` | An accepted answer is not acknowledged. For an input that hands its value on to another form's input (its `after` flow), whose own acceptance or rejection is what the caller hears. |
176
177
  | `response` | `{description}` on a readable leaf; use existing contextual DISPATCH extraction before accepting an open-ended answer. |
177
178
  | `proposal` | Optional approval of an inferred field candidate, independent of final action consent. |
178
179
  | `max_misses`, `max_attempts`, `on_exhausted` | Bounds for unreadable responses, validator rejections and deliberate branch recovery. |
@@ -214,7 +215,10 @@ Declarations:
214
215
  Rules, in order:
215
216
 
216
217
  1. **The current question first.** The whole answer is read against the current input's signature and the inputs it
217
- invites. A match is that answer.
218
+ invites (an invited input with its asked signature, not its out-of-order one). A match is that answer. A bare number
219
+ whose length is the current input's keypad length (`digits`) is its answer even when its reader rejects it: its
220
+ validation decides and the question is asked again, so an expiration "1320" (month 13) is never a security code.
221
+ A choice keeps rule 4, and an amount is read by its own profile.
218
222
  2. **A confirmation is answered only when it is asked.** Out of order it never makes sense, so no words, YES, NO or the
219
223
  option's own phrases, fill a confirmation that is not the current question.
220
224
  3. **Out of order requires a ready field and the whole signature.** A bare number never fills, out of order, a field whose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
5
5
  "license": "MIT",
6
6
  "author": "Ron Pinkas",