jsfe 1.15.8 → 1.16.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/README.md +1 -0
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +4 -2
- package/dist/forms/address-flows.js +1 -1
- package/dist/forms/continuation-compat.d.ts +5 -0
- package/dist/forms/continuation-compat.d.ts.map +1 -0
- package/dist/forms/continuation-compat.js +45 -0
- package/dist/forms/contracts.d.ts.map +1 -1
- package/dist/forms/contracts.js +6 -4
- package/dist/forms/declarations.d.ts +3 -0
- package/dist/forms/declarations.d.ts.map +1 -1
- package/dist/forms/declarations.js +27 -5
- package/dist/forms/flows.d.ts +2 -1
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +32 -15
- package/dist/forms/identity-flows.d.ts +1 -0
- package/dist/forms/identity-flows.d.ts.map +1 -1
- package/dist/forms/identity-flows.js +20 -3
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.js +1 -0
- package/dist/helpers/address.d.ts +1 -0
- package/dist/helpers/address.d.ts.map +1 -1
- package/dist/helpers/address.js +4 -1
- package/dist/helpers/answer.d.ts +2 -0
- package/dist/helpers/answer.d.ts.map +1 -1
- package/dist/helpers/answer.js +5 -0
- package/dist/helpers/form-actions.js +1 -1
- package/dist/helpers/form-conversation.d.ts +3 -0
- package/dist/helpers/form-conversation.d.ts.map +1 -1
- package/dist/helpers/form-conversation.js +18 -0
- package/dist/helpers/form-state.d.ts +2 -1
- package/dist/helpers/form-state.d.ts.map +1 -1
- package/dist/helpers/form-state.js +6 -2
- package/dist/helpers/index.d.ts +3 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +3 -3
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +44 -11
- package/dist/host/intent-stages.d.ts.map +1 -1
- package/dist/host/intent-stages.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/runtime-identity.js +1 -1
- package/docs/architecture-guide.md +366 -0
- package/docs/copilot-reference.md +5 -4
- package/docs/development-release.md +7 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ npm i jsfe
|
|
|
12
12
|
- **[JavaScript Flow Engine User Guide](user-guide.md)**
|
|
13
13
|
- Comprehensive tutorials, examples, and best practices
|
|
14
14
|
- **[README.md](README.md)** - Technical API reference (this document)
|
|
15
|
+
- **[Flows architecture guide](docs/architecture-guide.md)** - start here: the concepts, the building blocks, what the caller experiences, and how conversational AI and exact transactional flows work together (the reply ranker and DISPATCH)
|
|
15
16
|
- **[Flow author reference](docs/copilot-reference.md)** - the authoritative engine and JSON authoring specification for flow authors and AI flow generators (shipped in the package; the Flow Designer's CoPilot reads it from the published version, so change it with the engine)
|
|
16
17
|
- **[Development candidate acceptance](docs/development-release.md)** - reproducible package and cross-repository verification
|
|
17
18
|
- **[System Flows](docs/flows/system/README.md)** and **[Shopify Flows](docs/flows/shopify/README.md)** - the bundled flow and tool libraries in `flows/`: a page and a chart per flow, generated from the JSON (`node scripts/flow-docs.mjs`), plus the conventions the system flows share
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EAsRC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
|
+
import { resolveSetDataReferences } from '../forms/declarations.js';
|
|
17
18
|
export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log = console.log }) {
|
|
18
|
-
|
|
19
|
+
// A catalog or value written as {"$data": path} is documented as compilation resolves it.
|
|
20
|
+
const SETS = sets.map((s) => ({ ...s, flows: resolveSetDataReferences(s.flows) }));
|
|
19
21
|
// Which set holds a flow id; a page links to a flow in its own set, or in another set's folder.
|
|
20
22
|
const setOf = new Map(), flowOf = new Map();
|
|
21
23
|
for (const s of SETS) {
|
|
@@ -422,7 +424,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
422
424
|
}
|
|
423
425
|
else { /* not a compiled form */ }
|
|
424
426
|
if (flow.performs) {
|
|
425
|
-
out.push('', '### Performs', '', ...flow.performs.map((a) => `- \`${a.action}\` (${a.effect})
|
|
427
|
+
out.push('', '### Performs', '', ...flow.performs.map((a) => `- \`${a.action}\` (${a.effect})`), '', performsChart(flow));
|
|
426
428
|
}
|
|
427
429
|
else { /* not a performer */ }
|
|
428
430
|
if (flow.call) {
|
|
@@ -28,7 +28,7 @@ const street = C('address-street-validate', 'Validate a proposed street against
|
|
|
28
28
|
W('address_street_malformed', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", S('address_street_invalid', 'noop', "(cargo.inputCheck={ok:false,message:'Please give the street number and street name.',message_es:'Indique el número y nombre de la calle.'},true)")), W('address_street_stop', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", { id: 'address_street_invalid_end', type: 'END' }),
|
|
29
29
|
S('address_street_components', 'components', "'postal_code:'+values.zip+'|country:US'"), { id: 'address_street_tool', type: 'CALL-TOOL', tool: 'geocode-street', variable: 'result', args: { address: '{{value}}', components: '{{components}}' }, onFail: S('address_street_failed', 'result', 'null') },
|
|
30
30
|
S('address_street_projection', 'location', E(streetLocation, 'result', 'value', 'values.zip')),
|
|
31
|
-
S('address_street_result', 'noop', "(cargo.forms[form].data.street=location, cargo.inputCheck={ok:true,value:String(value)}, true)")
|
|
31
|
+
S('address_street_result', 'noop', "(cargo.forms[form].data.street=location, location.city&&cargo.forms[form].accepted&&cargo.forms[form].accepted.city&&cargo.forms[form].accepted.city.origin==='derived'&&cargo.forms[form].values.city!==location.city?(cargo.forms[form].values.city=location.city):null, cargo.inputCheck={ok:true,value:String(value)}, true)")
|
|
32
32
|
], { parameters: [P('form'), P('value'), P('values', 'object')], variables: V({ components: '', result: null, location: null }), contract: { tools: ['geocode-street'], result: { path: 'cargo.inputCheck', type: 'object' } } });
|
|
33
33
|
const zipPrompt = C('address-zip-prompt', 'Render the ZIP retry wording from the owned address instance.', [S('address_zip_prompt', 'noop', "(cargo.forms[form].data.zip_question=(cargo.forms[form].data.retry ? \"Let's try again. \" : '')+'What is the 5-digit zip code of your new address?',cargo.forms[form].data.zip_question_es=(cargo.forms[form].data.retry?'Intentémoslo otra vez. ':'')+'¿Cuál es el código postal de 5 dígitos de su nueva dirección?',true)")], { parameters: [P('form')], contract: { result: { path: 'cargo.forms[form].data', type: 'object' } } });
|
|
34
34
|
const streetPrompt = C('address-street-prompt', 'Include the ZIP-derived location when asking for the street.', [S('address_street_prompt', 'noop', "(cargo.forms[form].data.street_question=(cargo.forms[form].data.location?'Got it, '+(cargo.forms[form].values.city?cargo.forms[form].values.city+', '+cargo.forms[form].data.location.state:cargo.forms[form].data.location.line)+'. ':'')+'What is the street address?',cargo.forms[form].data.street_question_es=(cargo.forms[form].data.location?'Entendido, '+(cargo.forms[form].values.city?cargo.forms[form].values.city+', '+cargo.forms[form].data.location.state:cargo.forms[form].data.location.line)+'. ':'')+'¿Cuál es la dirección?',true)")], { parameters: [P('form')], contract: { result: { path: 'cargo.forms[form].data', type: 'object' } } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"continuation-compat.d.ts","sourceRoot":"","sources":["../../src/forms/continuation-compat.js"],"names":[],"mappings":"AAgBA;;;IAgBC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deploy check: a change that can break a saved session must bump its declaration's major version. A saved session
|
|
3
|
+
* continues across a minor or patch change, so an entry argument or a form input it may still hold must not disappear
|
|
4
|
+
* without a major bump; neither may a shared data or function name without its entry's major bump. Removing a flow needs
|
|
5
|
+
* no bump: a session standing in a flow that no longer exists is retired anyway.
|
|
6
|
+
*
|
|
7
|
+
* continuationBreaks(previousFlows, nextFlows) → [{ id, removed: { parameters?, inputs?, data?, functions? } }]
|
|
8
|
+
* both arrays are the authored declarations (a set's .flows file); ordinary executable flows are not compared.
|
|
9
|
+
* It reads names only: a change of meaning under the same name is still the author's to declare with a major bump.
|
|
10
|
+
*/
|
|
11
|
+
import { majorOf } from './declarations.js';
|
|
12
|
+
const parameterNames = parameters => Array.isArray(parameters) ? parameters.map(p => p?.name).filter(Boolean) : Object.keys(parameters || {});
|
|
13
|
+
const inputNames = inputs => (inputs || []).flatMap(input => [input?.name, ...inputNames(input?.any_of)]).filter(Boolean);
|
|
14
|
+
const missing = (before, after) => { const keep = new Set(after); return [...new Set(before)].filter(name => !keep.has(name)); };
|
|
15
|
+
export function continuationBreaks(previousFlows, nextFlows) {
|
|
16
|
+
const next = new Map((nextFlows || []).filter(flow => flow && flow.id).map(flow => [flow.id, flow]));
|
|
17
|
+
const breaks = [];
|
|
18
|
+
for (const before of previousFlows || []) {
|
|
19
|
+
const after = before && next.get(before.id);
|
|
20
|
+
if (!after || before.declarationVersion === undefined || majorOf(before.version) !== majorOf(after.version))
|
|
21
|
+
continue;
|
|
22
|
+
const removed = {};
|
|
23
|
+
if (before.library === 'data') {
|
|
24
|
+
const names = missing(Object.keys(before.data || {}), Object.keys(after.data || {}));
|
|
25
|
+
if (names.length)
|
|
26
|
+
removed.data = names;
|
|
27
|
+
}
|
|
28
|
+
else if (before.library === 'functions') {
|
|
29
|
+
const names = missing(Object.keys(before.functions || {}), Object.keys(after.functions || {}));
|
|
30
|
+
if (names.length)
|
|
31
|
+
removed.functions = names;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const parameters = missing(parameterNames(before.parameters), parameterNames(after.parameters));
|
|
35
|
+
if (parameters.length)
|
|
36
|
+
removed.parameters = parameters;
|
|
37
|
+
const inputs = missing(inputNames(before.form?.inputs), inputNames(after.form?.inputs));
|
|
38
|
+
if (inputs.length)
|
|
39
|
+
removed.inputs = inputs;
|
|
40
|
+
}
|
|
41
|
+
if (Object.keys(removed).length)
|
|
42
|
+
breaks.push({ id: before.id, removed });
|
|
43
|
+
}
|
|
44
|
+
return breaks;
|
|
45
|
+
}
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EC"}
|
package/dist/forms/contracts.js
CHANGED
|
@@ -138,7 +138,7 @@ export function defineProvider({ provides, contract, parameters = [], variables
|
|
|
138
138
|
}
|
|
139
139
|
export function defineForm({ contract, provides, ...spec }) {
|
|
140
140
|
const flow = formFlow({ ...spec, ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
|
|
141
|
-
return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' } });
|
|
141
|
+
return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' }, ...(provides ? { catalog: 'provides' } : {}) });
|
|
142
142
|
}
|
|
143
143
|
export function definePerformer({ performs, contract, prepare, present, project = [], provides, ...spec }) {
|
|
144
144
|
validateActionProjection(project, spec.id);
|
|
@@ -170,7 +170,7 @@ export function definePerformer({ performs, contract, prepare, present, project
|
|
|
170
170
|
{ ...set('receipt_applied', 'noop', 'pf_receipt ? consumeActionReceipt(cargo,pf_receipt) : true'), requireTrue: true },
|
|
171
171
|
call('present', present, { form: '{{form_instance}}', request: '{{pf_request}}', result: '{{pf_result}}' }),
|
|
172
172
|
...(spec.after || [])] });
|
|
173
|
-
return declared(flow, { role: 'performer', ...contract, requires: ['guardedActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection' });
|
|
173
|
+
return declared(flow, { role: 'performer', ...contract, requires: ['guardedActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection', ...(provides ? { catalog: 'provides' } : {}) });
|
|
174
174
|
}
|
|
175
175
|
/** A registered tool gets one generated adapter with explicit args; no dynamic tool-name interpreter is needed. */
|
|
176
176
|
export function operationWorkers(tools) {
|
|
@@ -278,6 +278,10 @@ export function validateFlowContracts(flows, tools = [], { externalFlows = [], r
|
|
|
278
278
|
for (const reference of [provides.gate?.flow, provides.source?.flow])
|
|
279
279
|
if (reference && !ids.has(reference))
|
|
280
280
|
fail(flow.id, `unknown provider worker ${reference}`);
|
|
281
|
+
// a FLOW to a name that does not exist is skipped silently at run time, so a menu choice's target is checked here
|
|
282
|
+
for (const choice of provides.menu?.choices || [])
|
|
283
|
+
if (choice?.flow && !ids.has(choice.flow))
|
|
284
|
+
fail(flow.id, `unknown menu worker ${choice.flow}`);
|
|
281
285
|
}
|
|
282
286
|
for (const action of contract.performs || flow.variables?.[contract.actions]?.value || []) {
|
|
283
287
|
if (action.retry?.unknown && !(action.effect === 'send' && action.retry.unknown === 'explicit'))
|
|
@@ -292,8 +296,6 @@ export function validateFlowContracts(flows, tools = [], { externalFlows = [], r
|
|
|
292
296
|
fail(flow.id, `unknown action worker ${action.flow}`);
|
|
293
297
|
if (!action.outcomes || !Object.keys(action.outcomes).length)
|
|
294
298
|
fail(flow.id, `action ${action.action} needs outcomes`);
|
|
295
|
-
if (['charge', 'write', 'send', 'cancel'].includes(action.effect) && !action.idempotency && !action.reconcile)
|
|
296
|
-
fail(flow.id, `action ${action.action} needs idempotency or reconciliation`);
|
|
297
299
|
if ((action.effect === 'charge' || action.effect === 'cancel' || action.reversible === false) && action.confirm?.exact !== true)
|
|
298
300
|
fail(flow.id, `action ${action.action} needs exact confirmation`);
|
|
299
301
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** The set's shared data for expressions, read as $data.<name>: references resolved, deep-frozen (one engine serves many
|
|
2
2
|
* sessions), never copied into session variables. Every $data.<name> an expression reads must exist, checked at load. */
|
|
3
3
|
export function flowSetData(flows: any, compiledFlows?: any[]): any;
|
|
4
|
+
export function resolveSetDataReferences(flows: any): any;
|
|
4
5
|
/** Compile only explicitly versioned declarations. Ordinary executable JSON is unchanged.
|
|
5
6
|
* @param {any[]} flows
|
|
6
7
|
* @param {{tools?: any[], externalFlows?: any[], validate?: boolean}} options
|
|
@@ -20,4 +21,6 @@ export function captureOriginalDeclaration(flow: any): {
|
|
|
20
21
|
export function serializeFlowDeclaration(flow: any, { resolvedFlow }?: {
|
|
21
22
|
resolvedFlow?: any;
|
|
22
23
|
}): any;
|
|
24
|
+
/** The major of a declared version ("2.4.1" → "2"; absent or unreadable → "0"). */
|
|
25
|
+
export function majorOf(version: any): string;
|
|
23
26
|
//# sourceMappingURL=declarations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"declarations.d.ts","sourceRoot":"","sources":["../../src/forms/declarations.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"declarations.d.ts","sourceRoot":"","sources":["../../src/forms/declarations.js"],"names":[],"mappings":"AAyDA;yHACyH;AACzH,oEAaC;AAID,0DAIC;AAmFD;;;;GAIG;AACH,+CAJW,GAAG,EAAE,uCACL;IAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,aAAa,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAC,GACxD,GAAG,EAAE,CAuEjB;AAED,sGAAsG;AACtG;;;SAAqK;AAuBrK,6FAA6F;AAC7F;;QAuCC;AAnSD,mFAAmF;AACnF,8CAA4G"}
|
|
@@ -4,7 +4,7 @@ import { linkFormSchemas, validateFlowContracts } from './contracts.js';
|
|
|
4
4
|
import { compileFormPrompts } from './prompt-compiler.js';
|
|
5
5
|
import { withSafetyRevision } from './continuation-safety.js';
|
|
6
6
|
import { capturedAuthorSpec, copyAuthorSpec, authoredExpressions } from './declaration-capture.js';
|
|
7
|
-
const formKeys = new Set(['inputs', 'before', 'after', 'confirm', 'before_confirm', 'declined', 'stop', 'close_on_stop', 'resume_on_parent_change', 'done', 'validator', 'confirmation_condition', 'prompt', 'prompt_es', 'incomplete_outcome', 'on_exhausted', 'must_complete']);
|
|
7
|
+
const formKeys = new Set(['inputs', 'before', 'after', 'confirm', 'before_confirm', 'declined', 'stop', 'close_on_stop', 'resume_on_parent_change', 'done', 'validator', 'confirmation_condition', 'prompt', 'prompt_es', 'incomplete_outcome', 'on_exhausted', 'on_no', 'must_complete']);
|
|
8
8
|
const sharedKeys = ['id', 'name', 'description', 'version', 'scope', 'primary', 'parameters', 'contract', 'parameterInstructions'];
|
|
9
9
|
const safe = /^[A-Za-z_$][\w$]*$/;
|
|
10
10
|
const fail = message => { throw new Error('Flow declaration: ' + message); };
|
|
@@ -12,6 +12,15 @@ const fail = message => { throw new Error('Flow declaration: ' + message); };
|
|
|
12
12
|
function sourceRevision(source) { const text = JSON.stringify(source); const hashes = [2166136261, 2246822507, 3266489909, 668265263]; for (let i = 0; i < text.length; i++)
|
|
13
13
|
for (let lane = 0; lane < hashes.length; lane++)
|
|
14
14
|
hashes[lane] = Math.imul(hashes[lane] ^ text.charCodeAt(i), 16777619 + lane * 2) >>> 0; return hashes.map(n => n.toString(16).padStart(8, '0')).join(''); }
|
|
15
|
+
/** The major of a declared version ("2.4.1" → "2"; absent or unreadable → "0"). */
|
|
16
|
+
const majorOf = version => { const m = /^\s*v?(\d+)/i.exec(String(version ?? '')); return m ? String(Number(m[1])) : '0'; };
|
|
17
|
+
/** A declaration's continuation revision: its major version, plus the major of each shared entry it uses ($data, fn.).
|
|
18
|
+
* A minor or patch change keeps the revision, so a saved session continues; a breaking change bumps the major. */
|
|
19
|
+
function majorRevision(declarationVersion, source, libraries) {
|
|
20
|
+
const text = JSON.stringify(source), uses = { data: /\$data\b/.test(text), functions: /(?<![\w$])fn\./.test(text) };
|
|
21
|
+
const shared = libraries.filter(library => uses[library.library]).map(library => library.library + '@' + library.id + ':' + majorOf(library.version)).sort();
|
|
22
|
+
return 'declaration-v' + declarationVersion + ':major:' + majorOf(source.version) + (shared.length ? '/' + shared.join('/') : '');
|
|
23
|
+
}
|
|
15
24
|
const variableDeclaration = value => value && typeof value === 'object' && !Array.isArray(value) && ['string', 'number', 'integer', 'boolean', 'object', 'array'].includes(value.type) ? value : { type: Array.isArray(value) ? 'array' : value === null || typeof value === 'object' ? 'object' : typeof value, value };
|
|
16
25
|
const DECLARATION_VERSIONS = new Set([1, 2]);
|
|
17
26
|
/** A flow set's shared entries: {id, library:'functions', declarationVersion:2, functions} and
|
|
@@ -29,7 +38,7 @@ function sharedFunctions(libraries) {
|
|
|
29
38
|
fail('a shared functions entry needs an id');
|
|
30
39
|
if (library.declarationVersion !== 2)
|
|
31
40
|
fail('shared functions ' + library.id + ' require declarationVersion 2');
|
|
32
|
-
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'functions'].includes(key));
|
|
41
|
+
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'version', 'functions'].includes(key));
|
|
33
42
|
if (extra.length)
|
|
34
43
|
fail('shared functions ' + library.id + ' cannot declare ' + extra.join(', '));
|
|
35
44
|
if (!library.functions || typeof library.functions !== 'object' || Array.isArray(library.functions) || !Object.keys(library.functions).length)
|
|
@@ -51,7 +60,7 @@ function sharedData(libraries) {
|
|
|
51
60
|
fail('a shared data entry needs an id');
|
|
52
61
|
if (library.declarationVersion !== 2)
|
|
53
62
|
fail('shared data ' + library.id + ' requires declarationVersion 2');
|
|
54
|
-
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'data'].includes(key));
|
|
63
|
+
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'version', 'data'].includes(key));
|
|
55
64
|
if (extra.length)
|
|
56
65
|
fail('shared data ' + library.id + ' cannot declare ' + extra.join(', '));
|
|
57
66
|
if (!library.data || typeof library.data !== 'object' || Array.isArray(library.data) || !Object.keys(library.data).length)
|
|
@@ -88,6 +97,15 @@ export function flowSetData(flows, compiledFlows = []) {
|
|
|
88
97
|
}
|
|
89
98
|
return Object.keys(resolved).length ? resolved : undefined;
|
|
90
99
|
}
|
|
100
|
+
/** A set's version 2 declarations with every {"$data":"path"} replaced by its value, as compilation sees them; the shared
|
|
101
|
+
* data entries are kept. For readers of the declarations (the flow docs); compilation resolves them itself. */
|
|
102
|
+
export { majorOf };
|
|
103
|
+
export function resolveSetDataReferences(flows) {
|
|
104
|
+
const data = sharedData((flows || []).filter(isLibrary));
|
|
105
|
+
if (!Object.keys(data).length)
|
|
106
|
+
return flows;
|
|
107
|
+
return flows.map(flow => flow?.declarationVersion === 2 && !isLibrary(flow) ? resolveDataReferences(flow, data, flow.id) : flow);
|
|
108
|
+
}
|
|
91
109
|
const isDataReference = value => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 1 && typeof value.$data === 'string';
|
|
92
110
|
/** Replace every {"$data":"path"} with a copy of the shared value at that path; an unknown path fails compilation. */
|
|
93
111
|
function resolveDataReferences(value, data, where, active = new Set()) {
|
|
@@ -266,7 +284,11 @@ export function compileFlowDeclarations(flows, { tools, externalFlows = [], vali
|
|
|
266
284
|
return false; parameterNames.add(parameter.name); return true; }), ...(rest.parameterInstructions !== undefined ? { parameterInstructions: rest.parameterInstructions } : {}), ...(functions ? { functions } : {}), metadata: { ...flow.metadata, declaration: { version: declarationVersion, source: source.id } } };
|
|
267
285
|
// Revisions are engine bookkeeping; a stale authored token cannot override source change detection.
|
|
268
286
|
const { safetyRevision: ignoredRevision, ...revisionSource } = source;
|
|
269
|
-
|
|
287
|
+
const revised = withSafetyRevision(flow, majorRevision(declarationVersion, original, libraries));
|
|
288
|
+
// The earlier revision, a fingerprint of the whole declaration: a session saved under it continues while the declaration
|
|
289
|
+
// is unchanged (and takes the major revision), so moving to major revisions retires no session.
|
|
290
|
+
revised.safetyRevisionCompat = 'declaration-v' + declarationVersion + ':' + sourceRevision(declarationVersion === 2 && Object.keys(shared).length ? { ...revisionSource, sharedFunctions: shared } : revisionSource);
|
|
291
|
+
return revised;
|
|
270
292
|
});
|
|
271
293
|
const externalObjects = externalFlows.filter(flow => flow && typeof flow === 'object');
|
|
272
294
|
const formIds = new Set(flows.filter(flow => flow.declarationVersion !== undefined && flow.form).map(flow => flow.id));
|
|
@@ -472,7 +494,7 @@ export function serializeFlowDeclaration(flow, { resolvedFlow = flow } = {}) {
|
|
|
472
494
|
else if (spec.variables)
|
|
473
495
|
result.variables = spec.variables;
|
|
474
496
|
for (const [key, value] of Object.entries(spec))
|
|
475
|
-
if (!sharedKeys.includes(key) && !['extraVars', 'variables', 'helpers', 'safetyRevision'].includes(key) && !(isForm && formKeys.has(key)))
|
|
497
|
+
if (!sharedKeys.includes(key) && !['extraVars', 'variables', 'helpers', 'safetyRevision', 'safetyRevisionCompat'].includes(key) && !(isForm && formKeys.has(key)))
|
|
476
498
|
result[key] = value;
|
|
477
499
|
const out = rewrite(result);
|
|
478
500
|
// Helpers and bodies can reference other explicitly authored expressions as well.
|
package/dist/forms/flows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, before_confirm, declined, stop, close_on_stop, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted, must_complete }: {
|
|
1
|
+
export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, before_confirm, declined, stop, close_on_stop, on_no, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted, must_complete }: {
|
|
2
2
|
id: any;
|
|
3
3
|
name: any;
|
|
4
4
|
version: any;
|
|
@@ -12,6 +12,7 @@ export function formFlow({ id, name, version, description, inputs, after, extraV
|
|
|
12
12
|
declined?: never[] | undefined;
|
|
13
13
|
stop?: string | undefined;
|
|
14
14
|
close_on_stop?: boolean | undefined;
|
|
15
|
+
on_no?: string | undefined;
|
|
15
16
|
resume_on_parent_change?: boolean | undefined;
|
|
16
17
|
before?: never[] | undefined;
|
|
17
18
|
done?: boolean | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAsfD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3rBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAo3B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -127,8 +127,10 @@ function validateAcceptanceDeclarations(id, inputs) {
|
|
|
127
127
|
else { /* no invites, or a list of names */ }
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, before_confirm = [], declined = [], stop = '', close_on_stop = false, resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false }) {
|
|
130
|
+
export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, before_confirm = [], declined = [], stop = '', close_on_stop = false, on_no = '', resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false }) {
|
|
131
131
|
validateFormExhaustion(on_exhausted);
|
|
132
|
+
if (on_no !== '' && (typeof on_no !== 'string' || !/^[\w-]+$/.test(on_no)))
|
|
133
|
+
throw new Error(`FORM ${id}: on_no must be a flow id`);
|
|
132
134
|
if (typeof must_complete !== 'boolean')
|
|
133
135
|
throw new Error(`FORM ${id}: must_complete must be true or false`);
|
|
134
136
|
validateAcceptanceDeclarations(id, inputs);
|
|
@@ -154,13 +156,13 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
154
156
|
] : [];
|
|
155
157
|
return {
|
|
156
158
|
id, name, version, description, scope, metadata: { formProtocol: 2 }, parameters: [...parameters, { name: 'form_instance', type: 'string', internal: true, description: 'Internal instance to resume' }], ...(primary ? { primary: true } : {}), ...(prompt ? { prompt, prompt_es } : {}),
|
|
157
|
-
variables: vars({ form_on_exhausted: on_exhausted, form_must_complete: must_complete, form_incomplete_outcome: incompleteFrom ? null : incomplete_outcome, form_instance: '', form_internal_resume: false, form_resume_owner: '', form_arguments: {}, form_exhaustion: null, form_values: {}, form_validation: '', initial_readings: [], inputs: resolveFormInputs(inputs, FIELD_PRESETS), parent_form: '', result_input: '', questions: [], provides: null, exit_context: '', exit_context_es: '', form_confirm_q: '', form_confirm_q_es: '', form_confirmation: confirm || {}, form_should_confirm: false, ...extraVars }),
|
|
159
|
+
variables: vars({ form_on_exhausted: on_exhausted, form_on_no: on_no, form_must_complete: must_complete, form_incomplete_outcome: incompleteFrom ? null : incomplete_outcome, form_instance: '', form_internal_resume: false, form_resume_owner: '', form_arguments: {}, form_exhaustion: null, form_values: {}, form_validation: '', initial_readings: [], inputs: resolveFormInputs(inputs, FIELD_PRESETS), parent_form: '', result_input: '', questions: [], provides: null, exit_context: '', exit_context_es: '', form_confirm_q: '', form_confirm_q_es: '', form_confirmation: confirm || {}, form_should_confirm: false, ...extraVars }),
|
|
158
160
|
steps: [
|
|
159
161
|
...contextualResponseRuntimeSteps(id, [], hasFormResponse(inputs) ? [CONTEXTUAL_RESPONSES_REQUIREMENT] : []),
|
|
160
162
|
...(hasFormResponse(inputs) ? [{ ...SET(`${id}_response_reader_required`, 'noop', "typeof validateFormResponse === 'function'"), requireTrue: true }] : []),
|
|
161
163
|
...proposalRuntimeSteps(id, [], hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []),
|
|
162
164
|
SET(`${id}_resume_kind`, 'form_internal_resume', '!!form_instance'),
|
|
163
|
-
SET(`${id}_open`, 'form_instance', `openFormInstance(cargo, ${JSON.stringify(id)}, inputs, { parent: parent_form || null, input: result_input || '', context:!!parent_form && !result_input, resume_on_parent_change: ${resume_on_parent_change === true}, resume: form_instance || (!parent_form ? (cargo.formTasks || {})[${JSON.stringify(id)}] || '' : ''), providers: provides, questions: questions, incomplete_outcome: ${incompleteExpression}, on_exhausted: form_on_exhausted, must_complete: form_must_complete })`),
|
|
165
|
+
SET(`${id}_open`, 'form_instance', `openFormInstance(cargo, ${JSON.stringify(id)}, inputs, { parent: parent_form || null, input: result_input || '', context:!!parent_form && !result_input, resume_on_parent_change: ${resume_on_parent_change === true}, resume: form_instance || (!parent_form ? (cargo.formTasks || {})[${JSON.stringify(id)}] || '' : ''), providers: provides, questions: questions, incomplete_outcome: ${incompleteExpression}, on_exhausted: form_on_exhausted, on_no: form_on_no, must_complete: form_must_complete })`),
|
|
164
166
|
SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
|
|
165
167
|
SIDE(`${id}_provider_reset`, '(delete cargo.forms[form_instance].providerRequests, delete cargo.forms[form_instance].activeGates, delete cargo.forms[form_instance].providerGateActive, true)'),
|
|
166
168
|
SIDE(`${id}_pass_reset`, '(cargo.forms[form_instance].replays = 0, form_internal_resume ? true : (cargo.forms[form_instance].attempts = {},cargo.forms[form_instance].misses={},cargo.forms[form_instance].feedback={}), true)'),
|
|
@@ -250,7 +252,7 @@ const collectInput = {
|
|
|
250
252
|
id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
|
|
251
253
|
description: "Worker: ask the form's current input once and file the answer wherever it belongs in the form chain (matchInputs, decided by scope; a declared confirm_against pair is asked). One reading → collect-input-accept; ambiguous → disambiguate-input; a question about the form's data → provide-data; a short miss → counted, the loop re-asks; 3+ words that fit nothing → DISPATCH. Params: form, current, questions, provides, exit_context(_es).",
|
|
252
254
|
parameters: ['form', 'current', 'questions', 'provides', 'exit_context', 'exit_context_es'].map((n) => ({ name: n, type: ['questions'].includes(n) ? 'array' : n === 'provides' ? 'object' : 'string', description: n })),
|
|
253
|
-
variables: vars({ ci_partial: '', ci_reply_reader: '', ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_must: false, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
|
|
255
|
+
variables: vars({ ci_partial: '', ci_no_owner: '', ci_no_flow: '', ci_no_up: false, ci_reply_reader: '', ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_must: false, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
|
|
254
256
|
steps: [
|
|
255
257
|
SET('ci_find_spec', 'ci_spec', "(function () { var s = ((cargo.forms[form] || {}).specs || []).filter(function (s) { return s.name === current; })[0]; return s ? (Array.isArray(s.any_of) ? Object.assign({}, s, { profile: { kind: 'group' } }) : s) : null; })()"),
|
|
256
258
|
SET('ci_current_names', 'ci_names', "Array.isArray(ci_spec && ci_spec.any_of) ? ci_spec.any_of.map(function (m) { return m.name; }) : [current]"),
|
|
@@ -299,6 +301,15 @@ const collectInput = {
|
|
|
299
301
|
// short of the input's minimum the next question is "Go on." Anything that is not digits drops the partial.
|
|
300
302
|
SET('ci_partial_join', 'ci_partial', "(function () { var state = cargo.forms[form]; if (!ci_spec || ci_spec.accumulate !== true || !ci_spec.digits) return ''; var said = String(cargo.choiceText || '').trim(); var data = state.data = state.data || {}; var key = '__partial_' + current; if (!/^[0-9][0-9\\s.,-]*$/.test(said)) { delete data[key]; return ''; } var joined = String(data[key] || '') + said.replace(/[^0-9]/g, ''); var starts = ci_spec.accumulate_first ? new RegExp(ci_spec.accumulate_first) : null; if (!data[key] && starts && !starts.test(joined)) return ''; if (joined.length < Number(ci_spec.digits.min || 0)) { data[key] = joined; var fb = state.feedback || {}; fb[current] = 'partial'; state.feedback = fb; return 'hold'; } delete data[key]; cargo.choiceText = joined; cargo.choice = joined; return 'joined'; })()"),
|
|
301
303
|
WHEN('ci_partial_hold', "ci_partial === 'hold'", END('ci_partial_wait')),
|
|
304
|
+
// A whole NO at a question that does not read it, in a form (or a sub-form of one) that declares `on_no`: that
|
|
305
|
+
// form's worker answers it (a payment starts its selection again), instead of any field or the reply ranker; the
|
|
306
|
+
// owning form then continues, a sub-form's owner included.
|
|
307
|
+
SET('ci_no_owner', 'ci_no_owner', "formNoOwner(cargo, form, ci_spec, cargo.choiceText)"),
|
|
308
|
+
SET('ci_no_flow', 'ci_no_flow', "ci_no_owner ? cargo.forms[ci_no_owner].on_no : ''"),
|
|
309
|
+
SET('ci_no_up', 'ci_no_up', "!!ci_no_owner && ci_no_owner !== form"), // before the call: a shared-scope worker may set `form`
|
|
310
|
+
WHEN('ci_no_run', 'ci_no_flow', FLOW('ci_no_answer', '{{ci_no_flow}}', 'call', { form: '{{ci_no_owner}}' })),
|
|
311
|
+
WHEN('ci_no_owner_resume', 'ci_no_flow && ci_no_up', FLOW('ci_no_resume_owner', 'resume-form', 'reboot', { form: '{{ci_no_owner}}' })),
|
|
312
|
+
WHEN('ci_no_end', 'ci_no_flow', END('ci_no_done')),
|
|
302
313
|
SET('ci_build_chain', 'ci_chain', CHAIN_JS),
|
|
303
314
|
SET('ci_match', 'ci_m', "readFormAnswer(cargo.choiceText, ci_chain, Array.isArray(ci_spec.any_of) ? { form: form, names: ci_names } : { form: form, name: current }, cargo.choice, provides && Array.isArray(questions) ? questions : [])"),
|
|
304
315
|
SIDE('ci_acceptance_trace', "(cargo.formAcceptance = formAcceptanceTrace(cargo.formAcceptance, ci_m), true)"),
|
|
@@ -407,9 +418,9 @@ const rankedChoices = (id, from) => SET(id, 'ranked', `(${from}).concat(${ESCAPE
|
|
|
407
418
|
const RANK_LABEL_JS = "function(c){var said=(Array.isArray(c.phrases) ? c.phrases : []).map(String).filter(function(p){return /[a-zà-ÿ]/i.test(p) && p!==String(c.key);}).slice(0,6);return String(c.label || (said.length ? c.key+' ('+said.join(', ')+')' : c.key));}";
|
|
408
419
|
// The choices of this form's OTHER inputs that the answer could also set (the word reader reads them all: 'LO VENCIDO' at
|
|
409
420
|
// the pay confirmation): the ranker is offered every choice the reader would accept (owner 2026-10-28). A confirmation
|
|
410
|
-
// input is answered only when asked,
|
|
411
|
-
// read as that input's answer.
|
|
412
|
-
const otherChoicesJS = (asked, taken) => `(function(){var seen=(${taken}).map(function(c){return String(c.key);}).concat(['yes','no']);return (chain || []).filter(function(i){return i.form===form && i.name!==${asked} && !i.confirmation && i.profile && i.profile.kind==='choice' && Array.isArray(i.profile.choices);}).reduce(function(all,i){return all.concat(i.profile.choices.filter(function(c){var k=String(c.key);if(seen.indexOf(k)>=0)return false;seen.push(k);return true;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c),owner:{form:i.form,name:i.name}};}));},[]);})()`;
|
|
421
|
+
// input is answered only when asked, a current_only choice only at its own question (as the reader reads it), and a key
|
|
422
|
+
// already offered is not offered twice. Each carries its input, so a pick is read as that input's answer.
|
|
423
|
+
const otherChoicesJS = (asked, taken) => `(function(){var seen=(${taken}).map(function(c){return String(c.key);}).concat(['yes','no']);return (chain || []).filter(function(i){return i.form===form && i.name!==${asked} && !i.confirmation && i.profile && i.profile.kind==='choice' && Array.isArray(i.profile.choices);}).reduce(function(all,i){return all.concat(i.profile.choices.filter(function(c){var k=String(c.key);if(c.current_only===true||seen.indexOf(k)>=0)return false;seen.push(k);return true;}).map(function(c){return {key:String(c.key),label:(${RANK_LABEL_JS})(c),owner:{form:i.form,name:i.name}};}));},[]);})()`;
|
|
413
424
|
const escapeRoutes = (id) => [WHEN(`${id}_agent`, "reply && reply.key === '__agent'", FLOW(`${id}_agent_flow`, 'live-agent-requested', 'reboot'))];
|
|
414
425
|
const rankedDispatch = (id, reason) => ({ ...yieldDispatch(id, 'form', `'${reason}'`), onResponse: 'continue', responseContext: 'Current question: {{asked}}', responseChoices: 'ranked', responseVariable: 'reply' });
|
|
415
426
|
const readFormReply = {
|
|
@@ -448,20 +459,26 @@ const readFormChoice = {
|
|
|
448
459
|
// form's other choices (a correction) and a person, never YES. Result: cargo.confirmRanked = 'no' | 'changed' | 'none'
|
|
449
460
|
// (ConfirmForm reads it); `abort` ends every flow; `new_request` goes to intent detection.
|
|
450
461
|
const readConfirmReply = {
|
|
451
|
-
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.
|
|
452
|
-
parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }],
|
|
453
|
-
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], picked: null, reading: null }),
|
|
462
|
+
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.3.0', scope: 'isolated', description: 'Rank an unreadable answer to a confirmation: it declines (NO), picks another of the form\'s choices (a correction, read as that input\'s answer), says a named field read back is wrong (confirm.named_fields: that input is asked again), asks for a person, or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
|
|
463
|
+
parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'named_fields', type: 'boolean' }],
|
|
464
|
+
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], fields: [], field: null, picked: null, reading: null, named_fields: false }),
|
|
454
465
|
steps: [SET('confirm_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
|
|
455
466
|
SET('confirm_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
|
|
456
467
|
SET('confirm_others', 'others', otherChoicesJS("''", '[]')),
|
|
457
|
-
|
|
468
|
+
// confirm.named_fields: a field read back may be corrected in the caller's own words ('es Long Beach' at an address
|
|
469
|
+
// read back with the ZIP code's city): each named input of this form that holds a value is offered, and a pick asks
|
|
470
|
+
// that input again, so the caller's value replaces the one read back
|
|
471
|
+
SET('confirm_fields', 'fields', "named_fields === true || named_fields === 'true' ? (chain || []).filter(function(i){var v=cargo.forms[form] && cargo.forms[form].values;return i.form===form && !i.confirmation && Array.isArray(i.names) && i.names.length && v && v[i.name]!==undefined && v[i.name]!==null && v[i.name]!=='';}).map(function(i){var l=String(i.label || i.names[0]);return {key:'field:'+i.name,label:'says the '+l+' read back is wrong, or gives a different '+l,owner:{form:i.form,name:i.name}};}) : []"),
|
|
472
|
+
rankedChoices('confirm_ranked_choices', "[{key:'no',label:'no: declines or refuses what the question asks to confirm'}].concat(others.map(function(c){return {key:c.key,label:c.label};})).concat(fields.map(function(c){return {key:c.key,label:c.label};}))"),
|
|
458
473
|
rankedDispatch('confirm_dispatch', 'confirmation_not_matched'),
|
|
459
474
|
...escapeRoutes('confirm_escape'),
|
|
460
475
|
// a correction picked by the ranker is read as its input's answer: the form changes and is confirmed again, never confirmed
|
|
461
476
|
SET('confirm_picked', 'picked', "others.filter(function(c){return reply && c.key===reply.key;})[0] || null"),
|
|
462
477
|
SET('confirm_read', 'reading', "picked ? enrichFormMatches(readFormAnswer(picked.key, chain, picked.owner, picked.key, []), chain, form, [picked.owner.name], picked.owner.name) : null"),
|
|
463
478
|
WHEN('confirm_change', 'reading && reading.hits.length', FLOW('confirm_change_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })),
|
|
464
|
-
|
|
479
|
+
SET('confirm_field', 'field', "fields.filter(function(c){return reply && c.key===reply.key;})[0] || null"),
|
|
480
|
+
WHEN('confirm_field_reset', 'field', SIDE('confirm_field_ask', '(resetFormInputs(cargo, field.owner.form, [field.owner.name]), true)')),
|
|
481
|
+
SIDE('confirm_ranked', "(cargo.confirmRanked=reading && reading.hits.length || field ? 'changed' : reply && reply.key==='no' ? 'no' : 'none',true)")]
|
|
465
482
|
};
|
|
466
483
|
const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
|
|
467
484
|
parameters: [...['question', 'question_es', 'description', 'context', 'context_es'].map(name => ({ name, type: 'string' })), { name: 'choices', type: 'array' }, { name: 'digits', type: 'object' }],
|
|
@@ -502,8 +519,8 @@ const acceptedEffects = {
|
|
|
502
519
|
WHEN('ie_none', '!ca_h', END('ie_end')),
|
|
503
520
|
SET('ie_rest', 'ca_rest', 'readings.slice(1)'),
|
|
504
521
|
WHEN('ie_reject', "ca_h.outcome === 'rejected'", SIDE('ie_attempt', "(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] = Number(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] || 0) + 1, cargo.forms[ca_h.form].feedback=cargo.forms[ca_h.form].feedback || {}, cargo.forms[ca_h.form].feedback[ca_h.asked || ca_h.name]='invalid', true)")),
|
|
505
|
-
SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' ? ('Okay, I noted your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
|
|
506
|
-
SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' ? ('Listo, anoté su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
|
|
522
|
+
SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' && ca_h.origin !== 'activation' ? ('Okay, I noted your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
|
|
523
|
+
SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' && ca_h.quiet ? '' : ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' && ca_h.origin !== 'activation' ? ('Listo, anoté su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
|
|
507
524
|
WHEN('ie_say', 'ca_ack', SAY('ie_ack', '{{ca_ack}}', '{{ca_ack_es}}')),
|
|
508
525
|
WHEN('ie_callback', "ca_h.after && ca_h.outcome !== 'known' && ca_h.outcome !== 'rejected' && ca_h.outcome !== 'pending'", FLOW('ie_after', '{{ca_h.after}}', 'call', { form: '{{ca_h.form}}', input: '{{ca_h.name}}', value: '{{ca_h.value}}' })),
|
|
509
526
|
WHEN('ie_more', 'ca_rest.length > 0', FLOW('ie_next', 'input-accepted-effects', 'replace', { readings: '{{ca_rest}}' })),
|
|
@@ -586,7 +603,7 @@ const confirmForm = {
|
|
|
586
603
|
'condition: cf_named': SET('cf_named_reset', 'cf_result', "(resetFormInputs(cargo, cf_named.form, [cf_named.name]), 'changed')"),
|
|
587
604
|
"condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
|
|
588
605
|
"condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',phrases:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
|
|
589
|
-
'condition: !cf_must && cf_words > 0': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}' }),
|
|
606
|
+
'condition: !cf_must && cf_words > 0': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', named_fields: '{{named_fields}}' }),
|
|
590
607
|
default: SET('cf_unclear', 'cf_result', "'unclear'")
|
|
591
608
|
} },
|
|
592
609
|
// the ranker's reading of an unread answer: a decline is the NO; an answer that declines nothing is asked again
|
|
@@ -5,6 +5,7 @@ export function serviceDigits(text: any): string;
|
|
|
5
5
|
export function otpDigits(text: any): string;
|
|
6
6
|
export function contactPrompt(cargo: any, options: any, callerAllowed: any, partial?: boolean, es?: boolean): string;
|
|
7
7
|
export function contactError(kind: any, options: any, es?: boolean): string;
|
|
8
|
+
export function otpWhere(cargo: any, es?: boolean): string;
|
|
8
9
|
export function otpPrompt(cargo: any, es?: boolean): string;
|
|
9
10
|
export function emailReadback(address: any, voice: any, es?: boolean): string;
|
|
10
11
|
/** Reenter the existing owning FORM or its gate without making the owner its own parent. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;
|
|
1
|
+
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAwFD,2DAKC;AACD,4DAMC;AAuHD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
|
|
@@ -95,6 +95,9 @@ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a suppli
|
|
|
95
95
|
when('identity_contact_partial_reset', '!contact_partial', effect('identity_contact_partial_forget', "(cargo.gcePartial='',true)")),
|
|
96
96
|
set('identity_contact_allowed', 'contact_allowed', "!!cargo.callerId && !callerIsInternational(cargo) && !cargo.callerDeclined && contact_options.caller_on_file !== 'no'"),
|
|
97
97
|
set('identity_contact_prefill', 'contact_prefill', "String(acct_number || cell_number || email || (!contact_partial && contact_allowed && contact_options.caller_on_file === 'yes' ? cargo.callerId : ''))"),
|
|
98
|
+
// a question answered from what is already known (the caller ID, a contact given) is never spoken: its intro (why the
|
|
99
|
+
// account is needed) leads the verification code question instead (otpPrompt). A spoken question said it (or had none).
|
|
100
|
+
effect('identity_contact_intro_unsaid', "(!contact_prefill ? (cargo.otpIntro='',cargo.otpIntro_es='') : contact_options.intro && !cargo.contactIntroSkip && !contact_options.fromMenu ? (cargo.otpIntro=String(contact_options.intro),cargo.otpIntro_es=String(contact_options.intro_es || contact_options.intro)) : true,true)"),
|
|
98
101
|
effect('identity_contact_preselected', "(acct_number || cell_number || email ? true : (cargo.callerPreselected=!!(contact_prefill && contact_options.caller_on_file === 'yes')),true)"),
|
|
99
102
|
set('identity_contact_profiles', 'contact_profiles', "contact_profiles_base.map(function (field) {return field.name === 'account' ? Object.assign({},field,{profile:{kind:'identifier',pattern:contact_options.account_pattern || '(?!)'}}) : field;}).concat(contact_allowed ? [{name:'caller_decision',label:'caller ID choice',label_es:'opción del identificador de llamadas',profile:{kind:'choice',choices:[{key:'yes',phrases:['1','yes','si','sí'],exact:true},{key:'no',phrases:['no','nope','nah'],exact:true}]}}] : [])"),
|
|
100
103
|
set('identity_contact_keypad', 'contact_keypad', "(function () {var shapes=[{first:'23456789',lengths:[10]},{first:'1',lengths:[11]}].concat(contact_options.account_shape || []);if(contact_partial){var left=[];(contact_options.account_shape || []).forEach(function(s){(s.lengths || []).forEach(function(n){var rest=n-String(cargo.gcePartial).length;if(rest>0 && left.indexOf(rest)<0)left.push(rest);});});return {min:1,max:12,autoSubmitMs:3000,complete:[{first:'0123456789',lengths:left.sort(function(a,b){return a-b;})}]};}return {min:7,max:12,autoSubmitMs:3000,autoSubmitChars:contact_allowed?['1']:[],complete:shapes};})()"),
|
|
@@ -171,16 +174,28 @@ const auth = worker('authenticate-user', 'AuthenticateUser', 'Authenticate a con
|
|
|
171
174
|
cases('auth_complete', { 'condition: cargo.otpVerified === true': set('auth_ok', 'auth_result', 'true'), 'condition: cargo.authRetry': end('auth_return_retry'), default: otpMenu('auth_retry_code', "I couldn't verify your account. You can try again or use another contact.", 'No pude verificar su cuenta. Puede intentarlo de nuevo o usar otro contacto.', true) }),
|
|
172
175
|
call('auth_gate_end', 'task-gate', { form: '{{parent_form}}', gate: 'authenticate-user', active: false })
|
|
173
176
|
] });
|
|
177
|
+
// Where the code went, for a caller who asks ('¿dónde está?'): the channel and the masked destination, never the code.
|
|
178
|
+
export function otpWhere(cargo, es = false) {
|
|
179
|
+
const number = String(cargo.otp_cell_number || '').replace(/\D/g, '').slice(-4), tail = cargo.voice ? number.split('').join(', ') : number;
|
|
180
|
+
if (cargo.otpSentOn === 'whatsapp')
|
|
181
|
+
return es ? 'Se lo acabo de enviar aquí en este chat.' : 'I just sent it to you here in this chat.';
|
|
182
|
+
if (cargo.otp_cell_number)
|
|
183
|
+
return es ? 'Se lo envié por mensaje de texto al celular que termina con ' + tail + '. Revise sus mensajes de texto.' : 'I sent it by text message to the cell ending with ' + tail + '. Check your text messages.';
|
|
184
|
+
return es ? 'Se lo envié por correo electrónico a ' + String(cargo.otp_email || '') + '. Revise su correo.' : 'I sent it by email to ' + String(cargo.otp_email || '') + '. Check your email.';
|
|
185
|
+
}
|
|
174
186
|
export function otpPrompt(cargo, es = false) {
|
|
175
187
|
const verb = es ? cargo.verb_es : cargo.verb, number = String(cargo.otp_cell_number || '').replace(/\D/g, '').slice(-4), tail = cargo.voice ? number.split('').join(', ') : number;
|
|
176
188
|
const where = cargo.otpSentOn === 'whatsapp' ? (es ? 'que le acabo de enviar aquí en este chat' : 'I just sent you here in this chat') : (es ? 'que recibió en ' : 'you received at ') + (cargo.otp_cell_number ? (es ? 'celular que termina con ' : 'cell ending with ') + tail : (es ? 'correo electrónico: ' : 'email: ') + String(cargo.otp_email || ''));
|
|
177
189
|
const name = cargo.callerPreselected && cargo.callerNamed && cargo.displayName ? cargo.displayName : '';
|
|
178
|
-
|
|
190
|
+
const lead = String((es ? cargo.otpIntro_es : cargo.otpIntro) || '');
|
|
191
|
+
return (lead ? lead + ' ' : '') + (name ? name + (es ? ', por favor' : ', please') : es ? 'Por favor' : 'Please') + ' ' + verb + ' ' + (cargo.voice ? es ? 'o ingrese ' : 'or enter ' : '') + (es ? 'el código de verificación de 6 dígitos ' : 'the 6-digit verification code ') + where + '. ' + (es ? `También puede ${cargo.voice ? 'presionar * o decir' : 'escribir'} SALIR para cancelar.` : `You can also ${cargo.voice ? 'press * or ' : ''}${verb} EXIT to cancel.`) + (cargo.callerPreselected ? (es ? ` Para usar otra cuenta, ${verb} OTRA CUENTA.` : ` To use another account, ${verb} ANOTHER ACCOUNT.`) : '');
|
|
179
192
|
}
|
|
180
193
|
const OTP_PROFILES = [{ name: 'code', label: 'verification code', label_es: 'código de verificación', profile: { kind: 'digits', min: 6, max: 6 } }, raw];
|
|
181
194
|
const code = worker('get-and-validate-otp-code', 'GetAndValidateOtpCode', 'Collect a one-time-code answer through the owning FORM ancestry, preserving protocol commands for validation.', { variables: vars({ otp_code: '', user_choice: '', normalized_otp_code: '', otp_question: '', otp_question_es: '', otp_profiles: OTP_PROFILES }), contract: { role: 'collector', summary: 'One-time-code answer collector using shared FORM matching' }, steps: [
|
|
182
195
|
when('otp_already_verified', 'cargo.otpVerified === true', end('otp_existing_verification')),
|
|
183
196
|
set('otp_question', 'otp_question', expression(otpPrompt, 'cargo')), set('otp_question_es', 'otp_question_es', expression(otpPrompt, 'cargo', 'true')),
|
|
197
|
+
// a contact question's unsaid intro (identity_contact_intro_unsaid) leads the first code question only
|
|
198
|
+
effect('otp_intro_said', "(cargo.otpIntro='',cargo.otpIntro_es='',true)"),
|
|
184
199
|
call('otp_collect', 'collect-service-answer', { profiles: '{{otp_profiles}}', question: '{{otp_question}}', question_es: '{{otp_question_es}}', keypad: { min: 6, max: 6 }, parent_form: '{{cargo.authOptions.parent_form}}', stop_path: 'cargo.otpVerified', context: 'account verification', context_es: 'verificación de cuenta' }),
|
|
185
200
|
when('otp_verified_by_join', 'cargo.otpVerified === true', end('otp_join_finished')),
|
|
186
201
|
set('otp_answer', 'otp_code', "String(cargo.serviceAnswer.values.code || cargo.serviceAnswer.raw || '')"), set('otp_choice', 'user_choice', 'otp_code.trim().toLowerCase()'), set('otp_digits', 'normalized_otp_code', expression(otpDigits, 'otp_code')),
|
|
@@ -206,8 +221,8 @@ function otpPhoneUnavailable(value) {
|
|
|
206
221
|
// What the reply ranker may take at the code question beside the code: the step's offers. Email and another account are
|
|
207
222
|
// offered only where the step offers them (a code sent by text; a caller-ID preselection).
|
|
208
223
|
const OTP_REPLY_CHOICES = [{ key: 'email', label: 'use the email on the account instead of the phone' }, { key: 'another_account', label: 'use another account' },
|
|
209
|
-
{ key: 'resend', label: 'the code did not arrive; send it again' }, { key: 'no_phone', label: 'no longer has that phone or number' }, { key: '__agent', label: 'speak with a person (a live agent)' }];
|
|
210
|
-
const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a challenge response with explicit bounded retries, alternate-contact handling and tenant callbacks.', { variables: vars({ otp_phrases: OTP_WORDS, no_code_given: false, wants_other_account: false, other_account_given: false, otp_validation_result: false, whatsapp_invite_sent: false, invite_text: '', otp_cancel: 'contact-support', answer_key: '', answer_value: '', otp_reply_choices: OTP_REPLY_CHOICES, otp_offers: [], otp_reply: {} }), contract: { role: 'gate', summary: 'Validate the active one-time challenge', tools: ['validate-otp', 'send-whatsapp-invite'], result: { path: 'cargo.otpVerified', type: 'boolean' } }, steps: [
|
|
224
|
+
{ key: 'where', label: 'asks where the code is or where it was sent, or cannot find it' }, { key: 'resend', label: 'the code did not arrive; send it again' }, { key: 'no_phone', label: 'no longer has that phone or number' }, { key: '__agent', label: 'speak with a person (a live agent)' }];
|
|
225
|
+
const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a challenge response with explicit bounded retries, alternate-contact handling and tenant callbacks.', { variables: vars({ otp_phrases: OTP_WORDS, no_code_given: false, wants_other_account: false, other_account_given: false, otp_validation_result: false, whatsapp_invite_sent: false, invite_text: '', otp_cancel: 'contact-support', answer_key: '', answer_value: '', otp_reply_choices: OTP_REPLY_CHOICES, otp_offers: [], otp_reply: {}, otp_where: '', otp_where_es: '' }), contract: { role: 'gate', summary: 'Validate the active one-time challenge', tools: ['validate-otp', 'send-whatsapp-invite'], result: { path: 'cargo.otpVerified', type: 'boolean' } }, steps: [
|
|
211
226
|
set('otp_cancel', 'otp_cancel', "cargo.authCancelFlow || 'contact-support'"),
|
|
212
227
|
set('otp_reset_no_code', 'no_code_given', 'false'), set('otp_reset_invite', 'whatsapp_invite_sent', 'false'), set('otp_reset_callback', 'answer_key', "''"),
|
|
213
228
|
when('otp_tenant_code', 'cargo.authOptions && cargo.authOptions.code_validator && normalized_otp_code.length !== 6 && /[0-9]/.test(user_choice)', call('otp_tenant_code_call', '{{cargo.authOptions.code_validator}}', { answer: '{{user_choice}}' })),
|
|
@@ -243,10 +258,12 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
|
|
|
243
258
|
+ ".concat(((typeof $callers !== 'undefined' && $callers && $callers['authenticate-user']) || []).map(function(c){return {key:'usecase:'+c.id,label:'states the reason for the call: '+c.label};}))"
|
|
244
259
|
+ ".concat(((typeof $gateExits !== 'undefined' && $gateExits && $gateExits['authenticate-user']) || []).map(function(a){return {key:'exit:'+a.key,label:a.label,route:'new_request'};}))"),
|
|
245
260
|
when('otp_off_topic', 'no_code_given', { id: 'otp_reply_rank', type: 'DISPATCH', onResponse: 'continue', responseContext: 'Please say the 6-digit verification code I sent you. (Por favor diga el código de verificación de 6 dígitos que le envié.)', responseChoices: 'otp_offers', responseVariable: 'otp_reply', onNoIntent: 'stay' }),
|
|
261
|
+
set('otp_where', 'otp_where', expression(otpWhere, 'cargo')), set('otp_where_es', 'otp_where_es', expression(otpWhere, 'cargo', 'true')),
|
|
246
262
|
set('otp_reply_picked', 'no_code_given', "no_code_given && !(otp_reply && otp_reply.key && String(otp_reply.key).indexOf('usecase:') !== 0)"),
|
|
247
263
|
cases('otp_reply_route', {
|
|
248
264
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'email'": call('otp_ranked_email', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
|
|
249
265
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'another_account'": call('otp_ranked_account', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
|
|
266
|
+
"condition: !no_code_given && otp_reply && otp_reply.key === 'where'": say('otp_ranked_where', '{{otp_where}}', '{{otp_where_es}}'),
|
|
250
267
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'resend'": call('otp_ranked_resend', 'resend-otp-code', { resend_reason: 'requested' }),
|
|
251
268
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'no_phone'": otpMenu('otp_ranked_no_phone', "No problem. Then the code can't reach you there. You can use the email on your account instead, or {{cargo.verb}} AGENT to reach a person.", 'No hay problema. Entonces el código no le puede llegar ahí. Puede usar el correo electrónico de su cuenta, o {{cargo.verb_es}} AGENTE para hablar con una persona.', false),
|
|
252
269
|
"condition: !no_code_given && otp_reply && otp_reply.key === '__agent'": call('otp_ranked_agent', 'live-agent-requested', undefined, 'reboot'),
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export const FLOWS: any[];
|
|
|
2
2
|
export { FLOWS as IDENTITY_FLOWS } from "./identity-flows.js";
|
|
3
3
|
export { compileFormPrompts } from "./prompt-compiler.js";
|
|
4
4
|
export { explainSession } from "./inspection.js";
|
|
5
|
+
export { continuationBreaks } from "./continuation-compat.js";
|
|
5
6
|
export { inheritFlowDeclaration } from "./declaration-capture.js";
|
|
6
7
|
export { withSafetyRevision, CONTINUATION_SAFETY_REQUIREMENT } from "./continuation-safety.js";
|
|
7
8
|
export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from "./flows.js";
|
package/dist/forms/index.js
CHANGED
|
@@ -13,4 +13,5 @@ export { addressDraft, zipLocation, streetLocation, resolvedAddress } from '../h
|
|
|
13
13
|
export { explainSession } from './inspection.js';
|
|
14
14
|
export { Form, Provider, Performer, Worker, compileExpression } from './authoring.js';
|
|
15
15
|
export { compileFlowDeclarations, serializeFlowDeclaration, captureOriginalDeclaration } from './declarations.js';
|
|
16
|
+
export { continuationBreaks } from './continuation-compat.js';
|
|
16
17
|
export { inheritFlowDeclaration } from './declaration-capture.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAAA,uFAAuF;AACvF;;;;;EAQC;AACD;;;;;SASC;AACD,gDAEC;AACD
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAAA,uFAAuF;AACvF;;;;;EAQC;AACD;;;;;SASC;AACD,gDAEC;AACD;;;;;EASC;AACD;;;;;;;;EAIC"}
|
package/dist/helpers/address.js
CHANGED
|
@@ -27,7 +27,10 @@ export function streetLocation(result, said, zip) {
|
|
|
27
27
|
const part = kind => components.find(c => (c.types || []).includes(kind))?.long_name || '';
|
|
28
28
|
const types = row.types || [], locationType = row.location_type || row.geometry?.location_type || '';
|
|
29
29
|
const exact = result?.status === 'OK' && row.partial_match !== true && types.includes('street_address') && locationType === 'ROOFTOP' && part('street_number') && part('route') && (!part('postal_code') || part('postal_code') === zip);
|
|
30
|
-
|
|
30
|
+
// An exact street match names the place its mail carries; it corrects a city read from the ZIP code alone (90805 is
|
|
31
|
+
// Lakewood to the ZIP lookup, while its streets are Long Beach and Lakewood)
|
|
32
|
+
const city = exact ? part('locality') || part('postal_town') || part('sublocality') : '';
|
|
33
|
+
return { street: exact ? part('street_number') + ' ' + part('route') : String(said).trim(), said: String(said).trim(), verified: !!exact, city };
|
|
31
34
|
}
|
|
32
35
|
export function resolvedAddress(values, data) {
|
|
33
36
|
const unit = /^(none|no|ninguno|ninguna|sin|no hay)$/i.test(String(values.unit || '')) ? '' : String(values.unit || '').replace(/^(apt\.?|unit|#)\s*/i, '');
|
package/dist/helpers/answer.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* YES, AGENT and TEXT never match a negated answer ("not sure", "I don't want an agent"); 0 is AGENT, * is EXIT.
|
|
4
4
|
*/
|
|
5
5
|
export function isAnswer(input: any, kind: any, extra?: any[]): boolean;
|
|
6
|
+
/** Whether the WHOLE answer is a NO ("No.", "no gracias", "nope, thanks"), never a sentence that starts with one. */
|
|
7
|
+
export function isWholeNo(input: any): boolean;
|
|
6
8
|
/**
|
|
7
9
|
* Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
|
|
8
10
|
* one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
|