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.
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +15 -1
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +2 -1
- package/dist/helpers/form-state.d.ts.map +1 -1
- package/dist/helpers/form-state.js +5 -1
- package/dist/helpers/match-inputs.d.ts +1 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +20 -3
- package/dist/runtime-identity.js +1 -1
- package/docs/copilot-reference.md +8 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EA+RC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -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
|
-
|
|
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;;;;;
|
|
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;
|
|
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
|
-
|
|
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
|
|
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])) : [];
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
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
|
|
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.
|
|
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",
|