jsfe 1.19.0 → 1.19.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/README.md CHANGED
@@ -398,6 +398,12 @@ reasonFor })` builds both executors' instructions. A reason question declares no
398
398
  unchanged: the generic withdrawal includes "I no longer want it", which at a reason question withdrew three prod
399
399
  cancellations.
400
400
 
401
+ **Digits said in groups (jsfe 1.19.1):** every `digits` input reads a comma, or a period and a space, after a group of
402
+ one to three digits as a pause inside one number, because speech-to-text writes spoken groups that way ("926, 1 0." is
403
+ the ZIP 92610, "7, 9, 984. 5." the code 799845). A comma after a longer number still separates two numbers ("92610,
404
+ 56035214"). At its own question, an answer of digits and separators alone is one number when its length fits ("4111,
405
+ 1111, 1111, 1111"). A choice still reads "1, 2" as two keys.
406
+
401
407
  **Choices that leave (`route: 'new_request'`) and staying on noise (`onNoIntent: 'stay'`, jsfe 1.15.3):** a choice
402
408
  declared `{ key, label, route: 'new_request' }` is offered like any other, and picking it leaves like a new request
403
409
  (intent detection decides where to). A contextual DISPATCH declared `onNoIntent: 'stay'` asks intent detection BEFORE
@@ -1 +1 @@
1
- {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAiFD;;;GAGG;AACH,sFA0GC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoOC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAuFD;;;GAGG;AACH,sFA8GC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoOC"}
@@ -33,10 +33,16 @@ export function formAcceptanceTrace(previous, matches) {
33
33
  function unescape(answer) {
34
34
  return String(answer ?? '').replace(/\\(['"\\])/g, '$1');
35
35
  }
36
- /** Digit runs as said: spoken digits come apart ("4 3 2 1", "555-123-4567"), so single spaces / dashes / dots inside a run join it. */
36
+ /** A run of digits as said: spoken digits come apart ("4 3 2 1", "555-123-4567"), so a single space, dash or dot inside a run
37
+ * joins it, and so does a comma (or a period and a space) after a group of one to three digits: speech-to-text writes the
38
+ * pauses between spoken groups that way (prod 2026-10-31: the ZIP 92610 said as "926, 1 0.", the code as "7, 9, 984. 5."). A comma after a
39
+ * longer number separates two numbers ("92610, 56035214"). */
40
+ const DIGIT_RUN = /\d(?:[\s.-]?\d|(?<=(?:^|\D)\d{1,3})(?:,\s?|\.\s)(?=\d))*/g;
37
41
  function digitRuns(raw) {
38
- return (raw.match(/\d(?:[\s.-]?\d)*/g) || []).map((r) => r.replace(/\D/g, ''));
42
+ return (raw.match(DIGIT_RUN) || []).map((r) => r.replace(/\D/g, ''));
39
43
  }
44
+ // A choice reads its keys without the comma join: "1, 2" names two keys, not 12.
45
+ const CHOICE_RUN = /\d(?:[\s.-]?\d)*/g;
40
46
  const DECIMAL_AMOUNT = /(?:[$€£]\s?)?\d+[.,]\d{2}(?!\d)/g; // a written decimal amount, with its currency mark
41
47
  /** Where the answer says amount n: a written decimal by itself ("51 72.45" is not one spoken run), else its number spans. */
42
48
  function amountSpans(raw, n) {
@@ -130,7 +136,7 @@ export function readInputProfile(profile, answer, isCurrent = true) {
130
136
  if (kind === 'choice') {
131
137
  // An amount's own digits are the amount's: a number beside it ("51:72.45", sub account 51 and $72.45) is still a choice.
132
138
  const free = isAmountAnswer(raw) ? amountMasked(raw) : raw;
133
- const runs = digitRuns(free), joined = free.replace(/\d(?:[\s.-]?\d)*/g, run => ' ' + run.replace(/\D/g, '') + ' ');
139
+ const runs = (free.match(CHOICE_RUN) || []).map((r) => r.replace(/\D/g, '')), joined = free.replace(CHOICE_RUN, run => ' ' + run.replace(/\D/g, '') + ' ');
134
140
  const choices = (Array.isArray(profile.choices) ? profile.choices : []).filter(c => isCurrent || c.current_only !== true).map(c => ({ ...c,
135
141
  numericWords: (c.phrases ?? c.words ?? []).filter(word => /^\d{2,}$/.test(String(word).trim())),
136
142
  phrases: (c.phrases ?? c.words ?? []).concat(isCurrent ? c.keys || [] : []).filter(word => !(free !== raw && /^\d+$/.test(String(word).trim()) && !runs.includes(String(word).trim())) && (isCurrent || profile.numeric_values === true || !/^[\d*#]$/.test(String(word).trim()))) }));
@@ -208,7 +214,14 @@ export function readInputProfile(profile, answer, isCurrent = true) {
208
214
  const min = Number(profile.min || 1);
209
215
  const max = Number(profile.max || min);
210
216
  // Out of order a decimal amount ("72.45", "287,15") is never this field's digits.
211
- const fits = [...new Set(digitRuns(isCurrent ? raw : raw.replace(DECIMAL_AMOUNT, (m) => ' '.repeat(m.length))).filter((d) => d.length >= min && d.length <= max))];
217
+ let fits = [...new Set(digitRuns(isCurrent ? raw : raw.replace(DECIMAL_AMOUNT, (m) => ' '.repeat(m.length))).filter((d) => d.length >= min && d.length <= max))];
218
+ // At its own question, an answer of digits and separators alone is one number when its length fits ("4111, 1111,
219
+ // 1111, 1111": a card read in groups of four).
220
+ const whole = raw.replace(/\D/g, '');
221
+ if (!fits.length && isCurrent && /^[\d\s.,-]+$/.test(raw.trim()) && whole.length >= min && whole.length <= max) {
222
+ fits = [whole];
223
+ }
224
+ else { /* read by runs */ }
212
225
  if (fits.length === 1) {
213
226
  return { value: fits[0], spans: numberSpans(raw, fits[0]), evidence: [`d:${fits[0]}`] };
214
227
  }
@@ -341,7 +354,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
341
354
  const repeatedChoice = known && !isCur(c) && c.profile.kind === 'choice';
342
355
  const isChoice = !isCur(c) && c.profile.kind === 'choice';
343
356
  const wholeOptions = isChoice ? (c.profile.choices || []).filter(choice => choice.key === option.value).map(choice => ({ ...choice, exact: true })) : [];
344
- const wholeChoice = isChoice && [raw, raw.replace(/\d(?:[\s.-]?\d)*/g, run => run.replace(/\D/g, ''))].some(text => pickChoice(text, wholeOptions, false) === option.value);
357
+ const wholeChoice = isChoice && [raw, raw.replace(CHOICE_RUN, run => run.replace(/\D/g, ''))].some(text => pickChoice(text, wholeOptions, false) === option.value);
345
358
  readings.push({ form: c.form, name: c.name, value: option.value, ...(option.refused === true ? { refused: true } : {}), ...(option.incompleteMoney === true ? { incompleteMoney: true } : {}), known, repeatedChoice, wholeChoice, choice: isChoice, narrative: isCur(c) && ['text', 'choice'].includes(c.profile.kind) && c.profile.narrative === true, resetOn: Array.isArray(c.reset_on) ? c.reset_on : [], spans: option.spans, keypad: option.keypad === true, fallback: option.fallback === true,
346
359
  ...(r.alternatives ? { option_label: option.option_label, option_label_es: option.option_label_es } : {}), group: c.group, exclusive: c.exclusive === true, level: forms.indexOf(c.scope || c.form), current: isCur(c), confirm: Array.isArray(c.confirm_against) ? c.confirm_against : [] });
347
360
  }
@@ -359,7 +372,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
359
372
  // so the last four of "88 91 7112" fill nothing. A written decimal is a run of its own ("51 72.45": 51, then 72.45).
360
373
  const decimalRuns = [...raw.matchAll(DECIMAL_AMOUNT)].map((m) => [m.index, m.index + m[0].length]);
361
374
  const undecimal = decimalRuns.reduce((text, [a, b]) => text.slice(0, a) + ' '.repeat(b - a) + text.slice(b), raw);
362
- const runs = [...undecimal.matchAll(/\d(?:[\s.-]?\d)*/g)].map((m) => [m.index, m.index + m[0].replace(/\s+$/, '').length]).concat(decimalRuns);
375
+ const runs = [...undecimal.matchAll(DIGIT_RUN)].map((m) => [m.index, m.index + m[0].replace(/\s+$/, '').length]).concat(decimalRuns);
363
376
  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));
364
377
  // Rule 4: a bare number has no money mark and no word in its evidence. Written cents are a money mark: "51:72.45" at
365
378
  // the sub account question is sub account 51 and the amount 72.45 (prod 2026-10-30).
@@ -371,7 +384,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
371
384
  // A choice is excluded (a digit it does not offer reaches the invited amount, rule 4), and so is an amount.
372
385
  const bareDigits = raw.replace(/\D/g, '');
373
386
  const shapeOf = own && own.digits && !['choice', 'amount', 'text'].includes(own.profile.kind) ? own.digits : null;
374
- if (shapeOf && bareDigits && /^[\d\s./-]+$/.test(raw.trim()) && !isAmountAnswer(raw) && bareDigits.length >= Number(shapeOf.min || 1) && bareDigits.length <= Number(shapeOf.max || shapeOf.min || 1)
387
+ if (shapeOf && bareDigits && /^[\d\s.,/-]+$/.test(raw.trim()) && !isAmountAnswer(raw) && bareDigits.length >= Number(shapeOf.min || 1) && bareDigits.length <= Number(shapeOf.max || shapeOf.min || 1)
375
388
  && !readings.some((reading) => reading.current || invited(reading))) {
376
389
  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,
377
390
  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 : [] });
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:85900ca9d0c36391776013b242bb7f81679da246eb6a5ffde8bb2888b523d2e1";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:f99ae1f2c338d0ef94a77bb5c074d8d015d853556943e2c26a3ecb6af5cc65df";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
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",