jsfe 1.29.0 → 1.30.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.
@@ -1 +1 @@
1
- {"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EAsRC"}
1
+ {"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EA+RC"}
@@ -410,7 +410,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
410
410
  }
411
411
  else { /* none */ }
412
412
  if (flow.provides) {
413
- out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), '', providerChart(flow.provides));
413
+ out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), ...providerMenuNotes(flow.provides.menu), '', providerChart(flow.provides));
414
414
  }
415
415
  else { /* not a provider */ }
416
416
  if (flow.form) {
@@ -441,6 +441,20 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
441
441
  else { /* no steps */ }
442
442
  return out.join('\n');
443
443
  }
444
+ // What a provider's menu opted into: both change what the caller hears, and neither shows in the chart.
445
+ function providerMenuNotes(menu) {
446
+ const notes = [];
447
+ if (menu && menu.whole_phrases) {
448
+ notes.push('- menu `whole_phrases`: a fact\'s phrase inside a sentence goes to the reply ranker; a fact pick also answers every fact that cleared 0.6');
449
+ }
450
+ else { /* not opted in */ }
451
+ const u = menu && menu.unanswered;
452
+ if (u) {
453
+ notes.push(`- menu \`unanswered\`: a question with no fact, back from this menu${u.asked ? ' (asked: ' + u.asked.join(', ') + ')' : ''}, says "${u.question}"${u.question_es ? ' / "' + u.question_es + '"' : ''}; yes runs menu choice \`${u.choice}\`, no returns to the menu`);
454
+ }
455
+ else { /* not opted in */ }
456
+ return notes;
457
+ }
444
458
  function upsert(file, title, generated, skeleton) {
445
459
  const block = `${START}\n${generated}\n${END}`;
446
460
  const old = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DC;AACD;;;GAGG;AACH;;;;;;EAGC;AA4gBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAltBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA26B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAsIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DC;AACD;;;GAGG;AACH;;;;;;EAGC;AA4gBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAntBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA46B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -84,7 +84,8 @@ export const FIELD_PRESETS = {
84
84
  card_expiration: { label: 'expiration date', label_es: 'fecha de vencimiento',
85
85
  // MM0YY: a zero said between month and year ("020 28", prod 2026-10-25 14:10, was taken for the billing ZIP).
86
86
  // Up to two words between month and year ("el mes 06 año 31", prod 2026-10-28 21:54, was asked again); a month by name.
87
- profile: { kind: 'pattern', regex: `\\b(0?[1-9]|1[0-2]|${MONTH_RE})\\.?(?:\\s*(?:/|-|del?|año|ano|year|of)(?=[\\s\\d])){0,2}\\s*(?:20)?(\\d{2})\\b|\\b0[1-9]0\\s+(?:20)?\\d{2}\\b` }, digits: { min: 3, max: 6 },
87
+ // A spoken separator with the digits apart: "0 4 guion 3 1" (owner 2026-11-02; prod 2026-11-01 15:19 was asked again).
88
+ profile: { kind: 'pattern', regex: `\\b(0?[1-9]|1[0-2]|${MONTH_RE})\\.?(?:\\s*(?:/|-|del?|año|ano|year|of)(?=[\\s\\d])){0,2}\\s*(?:20)?(\\d{2})\\b|\\b0[1-9]0\\s+(?:20)?\\d{2}\\b|\\b(?:0\\s?[1-9]|1\\s?[0-2]|[1-9])\\s*(?:gui[oó]n|dash|hyphen|slash|barra|diagonal)\\s*(?:2\\s?0\\s?)?\\d\\s?\\d\\b` }, digits: { min: 3, max: 6 },
88
89
  validator: 'validate-card-expiration',
89
90
  question: 'What is the expiration date, month and year?', question_es: '¿Cuál es la fecha de vencimiento, mes y año?',
90
91
  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.',
@@ -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;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA8CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;;;QAuCC;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;AAzpBM,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,uEA8CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAeC;AACD,uGAAuG;AACvG;;;;;;;;;;;;;;QAuCC;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;AA7pBM,8CAAmE"}
@@ -219,7 +219,7 @@ export function enrichFormMatches(matches, chain, form, names = [], asked = '')
219
219
  const enrich = h => {
220
220
  const spec = chain.find(c => c.form === h.form && c.name === h.name) || {};
221
221
  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 } : {}),
222
- 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 };
222
+ validator: spec.validator || '', ...(!(h.form === form && names.includes(h.name)) && spec.profile && !['choice', 'amount', 'text'].includes(spec.profile.kind) && /\d/.test(String(h.value)) ? { out_of_order_number: true } : {}), ...(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 };
223
223
  };
224
224
  return { ...matches, hits: matches.hits.map(enrich), ambiguous: matches.ambiguous.map(enrich), remaining: (matches.remaining || []).map(group => group.map(enrich)) };
225
225
  }
@@ -276,6 +276,10 @@ export function expandFormActivations(cargo, hits = []) {
276
276
  }
277
277
  /** No partial activation: validation failure suppresses inferred changes; pending prerequisites retain the complete group. */
278
278
  export function settleFormActivations(batch = {}, cargo = null) {
279
+ // A number read out of order is taken only when it is valid (owner 2026-11-02): one its validator rejects fills nothing and
280
+ // says nothing, and the current question is asked again.
281
+ const invalidNumbers = (batch.rejected || []).filter(h => h.out_of_order_number === true);
282
+ batch = { ...batch, rejected: (batch.rejected || []).filter(h => h.out_of_order_number !== true), ignored: [...(batch.ignored || []), ...invalidNumbers] };
279
283
  const accepted = [...(batch.accepted || [])], rejected = [...(batch.rejected || [])], pending = [...(batch.pending || [])];
280
284
  const all = accepted.concat(rejected, pending), keys = new Set(all.map(h => h.activation_group).filter(Boolean));
281
285
  const held = new Set(), discarded = new Set();
@@ -22,6 +22,7 @@ export function readInputProfile(profile: any, answer: any, isCurrent?: boolean)
22
22
  * 2. else the one input of the current form that fits takes it, and parent forms are not considered;
23
23
  * 3. else the same test one form up, and so on;
24
24
  * 4. two inputs at the nearest form that fits are ambiguous: asked, never ranked.
25
+ * Exception: a number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02).
25
26
  * Exception: an input that declares `confirm_against: [names]` is asked about together with those inputs whenever both
26
27
  * fit the same words, wherever scope would have put it ('62' at the payment offer: $62 or sub-account 62, prod
27
28
  * 2026-10-19 07:38).
@@ -1 +1 @@
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;;;;;;;;;;;;;;;;;;;;;;EA+PC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAwFD;;;GAGG;AACH,sFA8GC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EA6QC"}
@@ -56,7 +56,8 @@ function amountMasked(raw) {
56
56
  }
57
57
  /** Source intervals let different readers recognize the same evidence without agreeing on its type. */
58
58
  function numberSpans(raw, value, amount = false) {
59
- const runs = [...raw.matchAll(/\d(?:[\s.,-]?\d)*/g)];
59
+ // A field's number is located as its reader found it: DIGIT_RUN joins "31, 0 31". An unlocated number escaped every span rule.
60
+ const runs = [...raw.matchAll(amount ? /\d(?:[\s.,-]?\d)*/g : DIGIT_RUN)];
60
61
  if (!amount) {
61
62
  const wanted = String(value).replace(/\D/g, '');
62
63
  return runs.filter(m => { const d = m[0].replace(/\D/g, ''); return d === wanted || d === '1' + wanted; })
@@ -271,6 +272,7 @@ export function readInputProfile(profile, answer, isCurrent = true) {
271
272
  * 2. else the one input of the current form that fits takes it, and parent forms are not considered;
272
273
  * 3. else the same test one form up, and so on;
273
274
  * 4. two inputs at the nearest form that fits are ambiguous: asked, never ranked.
275
+ * Exception: a number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02).
274
276
  * Exception: an input that declares `confirm_against: [names]` is asked about together with those inputs whenever both
275
277
  * fit the same words, wherever scope would have put it ('62' at the payment offer: $62 or sub-account 62, prod
276
278
  * 2026-10-19 07:38).
@@ -368,7 +370,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
368
370
  // A declared confirm_against pair with the current question is relevant to it as well: the pair is asked about.
369
371
  const pairedWithCurrent = (reading) => list.some((c) => isCur(c) && c.form === reading.form && ((c.confirm_against || []).includes(reading.name) || (fieldOf(reading).confirm_against || []).includes(c.name)));
370
372
  const inOrder = (reading) => reading.current || invited(reading) || pairedWithCurrent(reading);
371
- // Rule 3: a number read out of order must be whole: every digit run its evidence touches lies entirely inside it,
373
+ // Rule 3, the whole number: every digit run a number read out of order touches lies entirely inside it,
372
374
  // 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).
373
375
  const decimalRuns = [...raw.matchAll(DECIMAL_AMOUNT)].map((m) => [m.index, m.index + m[0].length]);
374
376
  const undecimal = decimalRuns.reduce((text, [a, b]) => text.slice(0, a) + ' '.repeat(b - a) + text.slice(b), raw);
@@ -450,6 +452,20 @@ export function matchInputs(answer, candidates, current, choiceKey) {
450
452
  }
451
453
  // Rule 4: a single digit the current question offers as a key is that key (the keypad reading wins its group below);
452
454
  // one it does not offer reaches the invited amount, whose own validation decides ("3 dollars?").
455
+ // Rule 3, a perfect match (owner 2026-11-02): a number read out of order accounts for every digit of the answer. "0 4 guion
456
+ // 31, 0 31" at the expiration is not a ZIP 31031 and "31st. 9 3 1." not a code 931. Several numbers are a compound answer only
457
+ // when each is read, and a free-text answer owns its own digits. An amount has rule 4, a choice its declared phrases.
458
+ const readsDigits = (reading) => !['choice', 'amount', 'text'].includes(fieldOf(reading).profile.kind) && /\d/.test(spanText(reading) + String(reading.value));
459
+ const outOfOrderDigits = readings.filter((reading) => reading.rule === 'out_of_order' && readsDigits(reading));
460
+ const unread = unclaimedSpans(raw, readings.filter((reading) => reading.unlocated !== true)).map(([a, b]) => raw.slice(a, b)).join(' ');
461
+ if (outOfOrderDigits.length && /\d/.test(unread)) {
462
+ for (const reading of outOfOrderDigits)
463
+ refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'not_whole_answer', spans: reading.spans });
464
+ readings = readings.filter((reading) => !outOfOrderDigits.includes(reading));
465
+ }
466
+ else {
467
+ // no number read out of order, or nothing of the answer's digits is left over
468
+ }
453
469
  // profile.whole_phrases (a choice, or a pattern that reads words): a phrase picks only when the answer is accounted
454
470
  // for: its phrases, the arguments read beside them (numbers, amounts, a card), courtesy or connecting words
455
471
  // (isReaderFiller) and the field's own profile.template_words. A template such as "pay 50 with my Visa card" stays;
@@ -537,8 +553,9 @@ export function matchInputs(answer, candidates, current, choiceKey) {
537
553
  winners = members.filter((m) => m.current);
538
554
  }
539
555
  else {
556
+ // A number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02); scope settles words.
540
557
  const nearest = Math.min(...members.map((m) => m.level));
541
- winners = members.filter((m) => m.level === nearest);
558
+ winners = members.every(readsDigits) ? members : members.filter((m) => m.level === nearest);
542
559
  }
543
560
  // a winner declared to be confirmed against another member is asked about together with it
544
561
  const confirmWith = winners.length === 1 ? members.filter((m) => m !== winners[0] && paired(m, winners[0])) : [];
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:62d3efc670fd676fec2fb6fa94c06f160fd8f1e3a16881f38cae0d5af6419509";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:2bf72b6928fba83b16952cd064e11de4ecb2619129b5e8b8020ade6ecc3e9137";
@@ -232,16 +232,21 @@ Rules, in order:
232
232
  `depends_on` prerequisites are not filled yet (the sub-account before the account is known); a named answer ("50
233
233
  dollars") still waits as a pending value. **The whole signature:** A digit field takes a number only when the whole number fits its
234
234
  length; a pattern only when it covers the whole number; a choice only its declared or prepared values. A fragment of
235
- a longer number (the last four of "88 91 7112") fills nothing.
235
+ a longer number (the last four of "88 91 7112") fills nothing. **A perfect match:** a number read out of order accounts for every digit
236
+ of the answer, alone or with the other fields a compound answer fills. "0 4 guion 31, 0 31" at the expiration fills no ZIP;
237
+ "the line is 8185551234" fills the phone. Its validation decides too: a number its validator rejects fills nothing and says nothing, and the question
238
+ is asked again.
236
239
  4. **A bare number** (no money mark, no word naming a field) is an amount only at a question that is, invites or is
237
240
  declared `confirm_against` an amount. A single digit the question offers as a key is that key; one it does not offer reaches the invited amount,
238
- whose validation decides (Curacao asks for it in dollars). A bare number that does not fit the current question is
241
+ whose validation decides. The engine holds no amounts, so "only a known amount" is the amount validator's rule: Curacao's
242
+ takes a bare number at the amount question, and at a question that only invites an amount only when it is an amount the
243
+ caller can pay (else it asks for it in dollars). A bare number that does not fit the current question is
239
244
  read out of order under rule 3.
240
245
  5. **Compound answers.** Several fields fill from one answer only through the form's compound parser, or when every
241
246
  part of the answer fits exactly one field. **One part of an answer never fills two fields**; the engine refuses that
242
247
  reading and records the refusal.
243
248
  6. **Scope.** Across forms the nearest form wins; two fields of the same form that fit the same part are asked about,
244
- never ranked.
249
+ never ranked. A number that fits two fields out of order is asked about wherever they are.
245
250
 
246
251
  Every result records why: `decisions` lists each field filled other than as the current answer with its rule (`invited`, `out_of_order`, `compound`), and every
247
252
  refused reading is listed with its rule, so hosts can log what was filled out of order and what was refused.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.29.0",
3
+ "version": "1.30.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",