jsfe 1.15.0 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/docs/flow-docs.d.ts +13 -0
- package/dist/docs/flow-docs.d.ts.map +1 -0
- package/dist/docs/flow-docs.js +523 -0
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +12 -4
- package/dist/helpers/answer.d.ts +1 -1
- package/dist/helpers/answer.d.ts.map +1 -1
- package/dist/helpers/answer.js +7 -2
- package/dist/helpers/money.d.ts.map +1 -1
- package/dist/helpers/money.js +3 -2
- package/dist/runtime-identity.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm i jsfe
|
|
|
14
14
|
- **[README.md](README.md)** - Technical API reference (this document)
|
|
15
15
|
- **[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
16
|
- **[Development candidate acceptance](docs/development-release.md)** - reproducible package and cross-repository verification
|
|
17
|
-
- **[System Flows](docs/system
|
|
17
|
+
- **[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
|
|
18
18
|
|
|
19
19
|
*For detailed tutorials, step-by-step examples, and comprehensive workflow patterns, see the **[User Guide](user-guide.md)**.*
|
|
20
20
|
|
|
@@ -334,7 +334,7 @@ const body = buildAiRequest(
|
|
|
334
334
|
`toParameterRequest` / `fromParameterAnswers` (its enum and boolean parameters),
|
|
335
335
|
`freeTextParameters`, `mergeTextParameters`, `INTENT_YES_THRESHOLD`: these map the structured
|
|
336
336
|
intent-detection request to TypeSafe System One and back. The host sends the requests.
|
|
337
|
-
- Stage 1 asks one yes/no probability per flow and picks the highest if it reaches
|
|
337
|
+
- (The single-call mapping; two-stage detection uses `buildFlowStage`, below.) Stage 1 asks one yes/no probability per flow and picks the highest if it reaches
|
|
338
338
|
`INTENT_YES_THRESHOLD` (0.5), else `"None"`. Stage 2, for the chosen flow only, asks one per enum
|
|
339
339
|
value and one per boolean parameter. Below the threshold a parameter is omitted (the flow asks for
|
|
340
340
|
it), never set to `false`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function generateFlowDocs({ sets, root, check, hostRun, log }: {
|
|
2
|
+
sets: any;
|
|
3
|
+
root: any;
|
|
4
|
+
check?: boolean | undefined;
|
|
5
|
+
hostRun?: {} | undefined;
|
|
6
|
+
log?: {
|
|
7
|
+
(...data: any[]): void;
|
|
8
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
9
|
+
} | undefined;
|
|
10
|
+
}): {
|
|
11
|
+
failed: number;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=flow-docs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;EAqRC"}
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow docs: one Markdown page per flow (its facts: parameters, calls, tools, callers; and a Mermaid chart of each
|
|
3
|
+
* declaration: provider, form or compiled form inputs, performer, step list) and a README per set (its call graph and flow
|
|
4
|
+
* tables), written from the flow JSON itself so a chart can never disagree with a flow. Only the text between
|
|
5
|
+
* <!-- generated:start --> and <!-- generated:end --> is rewritten; anything else in a page is written by hand and kept.
|
|
6
|
+
*
|
|
7
|
+
* generateFlowDocs({ sets, root, check, hostRun, log }) → { failed }
|
|
8
|
+
* sets [{ key, title, flows, tools, source, how?, skeleton? }]: each set's pages go to <root>/<key>/; pages link
|
|
9
|
+
* across sets. skeleton: an entry point's page gets a "What it does" section to write by hand.
|
|
10
|
+
* check write nothing; count a stale page, a page with no flow (orphan) or a broken link as a failure.
|
|
11
|
+
* hostRun { flowId: 'who runs it' } for flows the host starts by id, outside any flow.
|
|
12
|
+
* A wrapper whose wrapped flow is an entry point inherits that role (the sets are composed that way).
|
|
13
|
+
* Node only (fs).
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log = console.log }) {
|
|
18
|
+
const SETS = sets;
|
|
19
|
+
// Which set holds a flow id; a page links to a flow in its own set, or in another set's folder.
|
|
20
|
+
const setOf = new Map(), flowOf = new Map();
|
|
21
|
+
for (const s of SETS) {
|
|
22
|
+
for (const f of s.flows) {
|
|
23
|
+
if (!setOf.has(f.id)) {
|
|
24
|
+
setOf.set(f.id, s.key);
|
|
25
|
+
flowOf.set(f.id, f);
|
|
26
|
+
}
|
|
27
|
+
else { /* the first set that defines it */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const known = (id) => setOf.has(id);
|
|
31
|
+
for (const s of SETS) {
|
|
32
|
+
for (const f of s.flows) {
|
|
33
|
+
const w = typeof f.wraps === 'string' ? flowOf.get(f.wraps) : null;
|
|
34
|
+
if (w && w !== f && w.primary && f.primary === undefined) {
|
|
35
|
+
f.primary = true;
|
|
36
|
+
f.inherited = f.wraps;
|
|
37
|
+
}
|
|
38
|
+
else { /* own role */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const START = '<!-- generated:start -->', END = '<!-- generated:end -->';
|
|
42
|
+
// The set whose page is being written: links are relative to its folder.
|
|
43
|
+
let current = SETS[0] ? SETS[0].key : '';
|
|
44
|
+
const href = (id) => (setOf.get(id) === current ? `${id}.md` : `../${setOf.get(id)}/${id}.md`);
|
|
45
|
+
const link = (id) => (known(id) ? `[${id}](${href(id)})${setOf.get(id) === current ? '' : ' (' + setOf.get(id) + ')'}` : id);
|
|
46
|
+
const esc = (s) => String(s).replace(/"/g, '#quot;').replace(/[\r\n]+/g, ' ').replace(/[<>]/g, (c) => (c === '<' ? '#lt;' : '#gt;')).replace(/[{}]/g, (c) => (c === '{' ? '#123;' : '#125;')).replace(/\|/g, '#124;').replace(/`/g, "'");
|
|
47
|
+
const cut = (s, n) => { const t = String(s).replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
48
|
+
const cond = (k) => cut(k.replace(/^condition:\s*/, ''), 70);
|
|
49
|
+
const safe = (s) => String(s).replace(/[^A-Za-z0-9]+/g, '_');
|
|
50
|
+
function walk(o, fn) { if (Array.isArray(o)) {
|
|
51
|
+
o.forEach((x) => walk(x, fn));
|
|
52
|
+
}
|
|
53
|
+
else if (o && typeof o === 'object') {
|
|
54
|
+
fn(o);
|
|
55
|
+
Object.values(o).forEach((v) => walk(v, fn));
|
|
56
|
+
}
|
|
57
|
+
else { /* scalar */ } }
|
|
58
|
+
const CALL_TYPES = new Set(['call', 'replace', 'reboot']);
|
|
59
|
+
// Keys of a declaration that name a flow by id: a provider's gate, source and menu flows, a performer's flow, prepare
|
|
60
|
+
// and present, a form input's sub-form, a call's flow, a wrapper's wrapped flow.
|
|
61
|
+
const FLOW_KEYS = { flow: 'declared', prepare: 'prepare', present: 'present', form: 'sub-form', wraps: 'wraps' };
|
|
62
|
+
// A flow is reached by a FLOW step, by a declaration key that names it, by its id passed as a FLOW step's parameter (a
|
|
63
|
+
// hook run by the flow it is passed to), or by its id written in a SET expression (a default target).
|
|
64
|
+
function callsOf(flow) {
|
|
65
|
+
const flows = new Map(), tools = new Set(), add = (id, how) => flows.set(id, (flows.get(id) || new Set()).add(how));
|
|
66
|
+
const { functions, provides, ...rest } = flow;
|
|
67
|
+
// A provider's gate and source are called; a menu choice's flow replaces the provider.
|
|
68
|
+
if (provides) {
|
|
69
|
+
if (provides.gate && provides.gate.flow) {
|
|
70
|
+
add(provides.gate.flow, 'call');
|
|
71
|
+
}
|
|
72
|
+
else { /* ungated */ }
|
|
73
|
+
if (provides.source && provides.source.flow) {
|
|
74
|
+
add(provides.source.flow, 'call');
|
|
75
|
+
}
|
|
76
|
+
else { /* no source */ }
|
|
77
|
+
for (const ch of (provides.menu && provides.menu.choices) || []) {
|
|
78
|
+
if (ch.flow) {
|
|
79
|
+
add(ch.flow, 'replace');
|
|
80
|
+
}
|
|
81
|
+
else { /* answers in place */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else { /* not a provider */ }
|
|
85
|
+
if (typeof flow.wraps === 'string') {
|
|
86
|
+
add(flow.wraps, 'wraps');
|
|
87
|
+
}
|
|
88
|
+
else { /* not a wrapper */ }
|
|
89
|
+
walk(rest, (s) => {
|
|
90
|
+
if (s.type === 'FLOW' && typeof s.value === 'string') {
|
|
91
|
+
add(s.value, s.callType || 'call');
|
|
92
|
+
for (const [k, v] of Object.entries(s.parameters || {})) {
|
|
93
|
+
if (typeof v === 'string' && v !== s.value && known(v)) {
|
|
94
|
+
add(v, `${k} of ${s.value}`);
|
|
95
|
+
}
|
|
96
|
+
else { /* not a flow name */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (s.type === 'SET' && typeof s.value === 'string') {
|
|
100
|
+
for (const m of s.value.matchAll(/'([a-z0-9]+(?:-[a-z0-9]+)+)'/g)) {
|
|
101
|
+
if (known(m[1]) && m[1] !== flow.id) {
|
|
102
|
+
add(m[1], 'by name');
|
|
103
|
+
}
|
|
104
|
+
else { /* not a flow id */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (!s.type) {
|
|
108
|
+
for (const [k, how] of Object.entries(FLOW_KEYS)) {
|
|
109
|
+
if (typeof s[k] === 'string' && known(s[k])) {
|
|
110
|
+
add(s[k], how === 'declared' ? 'call' : how);
|
|
111
|
+
}
|
|
112
|
+
else { /* not a flow reference */ }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else { /* not a flow reference */ }
|
|
116
|
+
// Any other key whose value is a flow id names that flow (a form's or input's validator, an input's after hook,
|
|
117
|
+
// a confirmation's on_declined).
|
|
118
|
+
for (const [k, v] of Object.entries(s)) {
|
|
119
|
+
if (typeof v === 'string' && v !== flow.id && known(v) && !(k in FLOW_KEYS) && !(s.type === 'FLOW' && k === 'value')) {
|
|
120
|
+
add(v, k);
|
|
121
|
+
}
|
|
122
|
+
else { /* not a flow id */ }
|
|
123
|
+
}
|
|
124
|
+
if (s.type === 'CALL-TOOL' && s.tool) {
|
|
125
|
+
tools.add(s.tool);
|
|
126
|
+
}
|
|
127
|
+
else if (!s.type && typeof s.tool === 'string' && s.action) {
|
|
128
|
+
tools.add(s.tool);
|
|
129
|
+
}
|
|
130
|
+
else { /* not a tool */ }
|
|
131
|
+
});
|
|
132
|
+
// The engine compiles a form's declined steps into its companion flow <id>-declined.
|
|
133
|
+
if (flow.form && Array.isArray(flow.form.declined) && flow.form.declined.length && known(flow.id + '-declined')) {
|
|
134
|
+
add(flow.id + '-declined', 'declined');
|
|
135
|
+
}
|
|
136
|
+
else { /* no companion */ }
|
|
137
|
+
// A function body names a flow by its quoted id (a proposal's on_declined, a default target).
|
|
138
|
+
for (const body of Object.values(functions || {})) {
|
|
139
|
+
for (const m of [].concat(body.body || body).join('\n').matchAll(/'([a-z0-9]+(?:-[a-z0-9]+)+)'/g)) {
|
|
140
|
+
if (known(m[1]) && m[1] !== flow.id) {
|
|
141
|
+
add(m[1], 'by name');
|
|
142
|
+
}
|
|
143
|
+
else { /* not a flow id */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { flows, tools };
|
|
147
|
+
}
|
|
148
|
+
const callers = new Map();
|
|
149
|
+
for (const f of SETS.flatMap((s) => s.flows)) {
|
|
150
|
+
for (const [target, hows] of callsOf(f).flows) {
|
|
151
|
+
const m = callers.get(target) || new Map();
|
|
152
|
+
for (const h of hows) {
|
|
153
|
+
m.set(f.id, (m.get(f.id) || new Set()).add(h));
|
|
154
|
+
}
|
|
155
|
+
callers.set(target, m);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function kindOf(flow) {
|
|
159
|
+
const k = [];
|
|
160
|
+
if (flow.provides) {
|
|
161
|
+
k.push('provider');
|
|
162
|
+
}
|
|
163
|
+
else { /* none */ }
|
|
164
|
+
if (flow.form || (flow.metadata && flow.metadata.formProtocol)) {
|
|
165
|
+
k.push('form');
|
|
166
|
+
}
|
|
167
|
+
else { /* none */ }
|
|
168
|
+
if (flow.performs) {
|
|
169
|
+
k.push('performer');
|
|
170
|
+
}
|
|
171
|
+
else { /* none */ }
|
|
172
|
+
if (flow.call) {
|
|
173
|
+
k.push('call');
|
|
174
|
+
}
|
|
175
|
+
else { /* none */ }
|
|
176
|
+
if (flow.wraps) {
|
|
177
|
+
k.push('wrapper');
|
|
178
|
+
}
|
|
179
|
+
else { /* none */ }
|
|
180
|
+
if (flow.steps && flow.steps.length) {
|
|
181
|
+
k.push('steps');
|
|
182
|
+
}
|
|
183
|
+
else { /* none */ }
|
|
184
|
+
if (flow.functions && !k.length) {
|
|
185
|
+
k.push('function library');
|
|
186
|
+
}
|
|
187
|
+
else { /* none */ }
|
|
188
|
+
if (flow.data && !k.length) {
|
|
189
|
+
k.push('data');
|
|
190
|
+
}
|
|
191
|
+
else { /* none */ }
|
|
192
|
+
return k;
|
|
193
|
+
}
|
|
194
|
+
// A step as a node label; terminal = control leaves this flow there.
|
|
195
|
+
function describe(s) {
|
|
196
|
+
const t = s.type;
|
|
197
|
+
if (t === 'SET') {
|
|
198
|
+
return { label: 'set ' + s.variable, kind: 'set' };
|
|
199
|
+
}
|
|
200
|
+
if (t === 'SAY') {
|
|
201
|
+
return { label: 'say: ' + cut(s.value || '', 60), kind: 'say' };
|
|
202
|
+
}
|
|
203
|
+
if (t === 'SAY-GET') {
|
|
204
|
+
return { label: 'ask: ' + cut(s.value || '', 60), kind: 'ask' };
|
|
205
|
+
}
|
|
206
|
+
if (t === 'CALL-TOOL') {
|
|
207
|
+
return { label: 'tool: ' + s.tool, kind: 'tool' };
|
|
208
|
+
}
|
|
209
|
+
if (t === 'FLOW') {
|
|
210
|
+
const ct = s.callType || 'call';
|
|
211
|
+
const q = s.value === 'ask-choice' && s.parameters && typeof s.parameters.question === 'string' ? ' ' + cut(s.parameters.question.replace(/^\{\{(.*)\}\}$/, '$1'), 50) : '';
|
|
212
|
+
return { label: (ct === 'call' ? 'call ' : ct + ' → ') + s.value + q, kind: ct === 'call' ? 'call' : 'exit' };
|
|
213
|
+
}
|
|
214
|
+
if (t === 'END') {
|
|
215
|
+
return { label: 'end (back to caller)', kind: 'exit' };
|
|
216
|
+
}
|
|
217
|
+
if (t === 'RETURN') {
|
|
218
|
+
return { label: 'return to host / AI', kind: 'exit' };
|
|
219
|
+
}
|
|
220
|
+
if (t === 'DISPATCH') {
|
|
221
|
+
return { label: 'dispatch: intent detection', kind: 'exit' };
|
|
222
|
+
}
|
|
223
|
+
return { label: t, kind: 'other' };
|
|
224
|
+
}
|
|
225
|
+
// One chart builder for every shape: nodes and edges, with the open ends ("pending") joined to whatever comes next.
|
|
226
|
+
function builder() {
|
|
227
|
+
const lines = ['flowchart TD', ' START(("start"))'];
|
|
228
|
+
let pending = [['START', null]], seq = 0;
|
|
229
|
+
const id = (p) => `${p}${seq++}`;
|
|
230
|
+
const node = (nid, d) => {
|
|
231
|
+
const shape = d.kind === 'exit' ? [`(["`, `"])`] : d.kind === 'tool' ? [`[/"`, `"/]`] : d.kind === 'call' ? [`[["`, `"]]`] : d.kind === 'input' ? [`[/"`, `"\\]`] : d.kind === 'decision' ? [`{"`, `"}`] : [`["`, `"]`];
|
|
232
|
+
lines.push(` ${nid}${shape[0]}${esc(d.label)}${shape[1]}`);
|
|
233
|
+
const cls = { exit: 'endstep', tool: 'toolstep', call: 'flowstep', input: 'inputstep' }[d.kind];
|
|
234
|
+
if (cls) {
|
|
235
|
+
lines.push(` class ${nid} ${cls}`);
|
|
236
|
+
}
|
|
237
|
+
else { /* plain */ }
|
|
238
|
+
};
|
|
239
|
+
const join = (nid) => { for (const [from, label] of pending.splice(0)) {
|
|
240
|
+
lines.push(label ? ` ${from} -- "${esc(label)}" --> ${nid}` : ` ${from} --> ${nid}`);
|
|
241
|
+
} };
|
|
242
|
+
// Chain one plain node after the open ends.
|
|
243
|
+
const then = (d) => { const nid = id('n'); node(nid, d); join(nid); if (d.kind !== 'exit') {
|
|
244
|
+
pending.push([nid, null]);
|
|
245
|
+
}
|
|
246
|
+
else { /* leaves */ } return nid; };
|
|
247
|
+
// A fan: one decision, one labelled branch per entry, each branch's end left open (or closed when it leaves).
|
|
248
|
+
const fan = (label, branches) => {
|
|
249
|
+
const did = id('d');
|
|
250
|
+
node(did, { label, kind: 'decision' });
|
|
251
|
+
join(did);
|
|
252
|
+
for (const [edge, d] of branches) {
|
|
253
|
+
const bid = id('b');
|
|
254
|
+
node(bid, d);
|
|
255
|
+
lines.push(` ${did} -- "${esc(edge)}" --> ${bid}`);
|
|
256
|
+
if (d.kind !== 'exit') {
|
|
257
|
+
pending.push([bid, null]);
|
|
258
|
+
}
|
|
259
|
+
else { /* leaves */ }
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
// A step list: consecutive SETs collapse into one node; a CASE is a diamond with one edge per branch.
|
|
263
|
+
const steps = (list) => {
|
|
264
|
+
const groups = [];
|
|
265
|
+
for (const s of list || []) {
|
|
266
|
+
const last = groups[groups.length - 1];
|
|
267
|
+
if (s.type === 'SET' && last && last.set) {
|
|
268
|
+
last.vars.push(s.variable);
|
|
269
|
+
}
|
|
270
|
+
else if (s.type === 'SET') {
|
|
271
|
+
groups.push({ set: true, vars: [s.variable] });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
groups.push({ step: s });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const onFail = (nid, s) => {
|
|
278
|
+
if (s.type !== 'CALL-TOOL' || !s.onFail) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
else { /* a failure path */ }
|
|
282
|
+
const fid = nid + 'f', d = describe(s.onFail);
|
|
283
|
+
node(fid, d);
|
|
284
|
+
lines.push(` ${nid} -- "fails" --> ${fid}`);
|
|
285
|
+
if (d.kind !== 'exit') {
|
|
286
|
+
pending.push([fid, null]);
|
|
287
|
+
}
|
|
288
|
+
else { /* leaves the flow */ }
|
|
289
|
+
};
|
|
290
|
+
for (const g of groups) {
|
|
291
|
+
if (g.set) {
|
|
292
|
+
then({ label: 'set ' + cut([...new Set(g.vars)].filter((v) => v !== 'noop').join(', ') || 'noop', 70), kind: 'set' });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
else { /* a step */ }
|
|
296
|
+
const s = g.step;
|
|
297
|
+
if (s.type === 'CASE') {
|
|
298
|
+
const cid = id('c');
|
|
299
|
+
lines.push(` ${cid}{"case"}`);
|
|
300
|
+
join(cid);
|
|
301
|
+
Object.entries(s.branches || {}).forEach(([k, b]) => {
|
|
302
|
+
const edge = k === 'default' ? 'else' : cond(k);
|
|
303
|
+
const noop = b.type === 'SET' && /^noop/.test(b.variable || '');
|
|
304
|
+
if (noop) {
|
|
305
|
+
pending.push([cid, edge]);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
else { /* a real branch */ }
|
|
309
|
+
const bid = id('b'), d = describe(b);
|
|
310
|
+
node(bid, d);
|
|
311
|
+
lines.push(` ${cid} -- "${esc(edge)}" --> ${bid}`);
|
|
312
|
+
if (d.kind !== 'exit') {
|
|
313
|
+
pending.push([bid, null]);
|
|
314
|
+
}
|
|
315
|
+
else { /* leaves the flow */ }
|
|
316
|
+
onFail(bid, b);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const nid = then(describe(s));
|
|
321
|
+
onFail(nid, s);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const done = () => {
|
|
326
|
+
if (pending.length) {
|
|
327
|
+
lines.push(' DONE(["end (back to caller)"])', ' class DONE endstep');
|
|
328
|
+
join('DONE');
|
|
329
|
+
}
|
|
330
|
+
else { /* every path leaves explicitly */ }
|
|
331
|
+
lines.push(' classDef endstep fill:#eef,stroke:#669', ' classDef toolstep fill:#efe,stroke:#696', ' classDef flowstep fill:#fef6e0,stroke:#b80', ' classDef inputstep fill:#f3eefc,stroke:#75a');
|
|
332
|
+
return '```mermaid\n' + lines.join('\n') + '\n```';
|
|
333
|
+
};
|
|
334
|
+
return { then, fan, steps, done };
|
|
335
|
+
}
|
|
336
|
+
function providerChart(p) {
|
|
337
|
+
const c = builder();
|
|
338
|
+
if (p.gate && p.gate.flow) {
|
|
339
|
+
c.then({ label: `gate: call ${p.gate.flow} unless ${(p.gate.verified || []).join(' and ')}`, kind: 'call' });
|
|
340
|
+
}
|
|
341
|
+
else { /* ungated */ }
|
|
342
|
+
if (p.source && p.source.flow) {
|
|
343
|
+
c.then({ label: `source: call ${p.source.flow} → ${p.source.result || ''}${p.source.refresh ? ' (' + p.source.refresh + ')' : ''}`, kind: 'call' });
|
|
344
|
+
}
|
|
345
|
+
else { /* no source */ }
|
|
346
|
+
const facts = (p.facts || []).map((f) => f.key);
|
|
347
|
+
c.then({ label: `say facts: ${cut(facts.join(', '), 80)}${p.default ? ' (default ' + p.default + ')' : ''}`, kind: 'say' });
|
|
348
|
+
if (p.menu && (p.menu.choices || []).length) {
|
|
349
|
+
c.fan('menu', [...p.menu.choices.map((ch) => [cut((ch.phrases || []).slice(0, 3).join(' / ') || ch.key, 40), { label: `${ch.key} → ${ch.flow || ''}`, kind: ch.flow ? 'exit' : 'say' }]),
|
|
350
|
+
...(p.menu.done ? [[cut((p.menu.done.phrases || []).slice(0, 3).join(' / '), 40), { label: 'done', kind: 'exit' }]] : [])]);
|
|
351
|
+
}
|
|
352
|
+
else { /* no menu */ }
|
|
353
|
+
return c.done();
|
|
354
|
+
}
|
|
355
|
+
function formChart(fm) {
|
|
356
|
+
const c = builder();
|
|
357
|
+
if (fm.before && fm.before.length) {
|
|
358
|
+
c.steps(fm.before);
|
|
359
|
+
}
|
|
360
|
+
else { /* nothing before */ }
|
|
361
|
+
for (const i of fm.inputs || []) {
|
|
362
|
+
const extra = [i.when ? 'when ' + cut(i.when, 40) : '', i.form ? 'form ' + i.form : '', i.profile && i.profile.kind ? i.profile.kind : ''].filter(Boolean).join(' · ');
|
|
363
|
+
c.then({ label: `${i.name}${i.label ? ' (' + i.label + ')' : ''}${extra ? ': ' + extra : ''}`, kind: 'input' });
|
|
364
|
+
}
|
|
365
|
+
if (fm.confirm) {
|
|
366
|
+
c.then({ label: 'confirm', kind: 'decision' });
|
|
367
|
+
}
|
|
368
|
+
else { /* no confirmation */ }
|
|
369
|
+
if (fm.after && fm.after.length) {
|
|
370
|
+
c.steps(fm.after);
|
|
371
|
+
}
|
|
372
|
+
else { /* nothing after */ }
|
|
373
|
+
return c.done();
|
|
374
|
+
}
|
|
375
|
+
function performsChart(flow) {
|
|
376
|
+
const c = builder();
|
|
377
|
+
if (flow.prepare) {
|
|
378
|
+
c.then({ label: 'prepare: call ' + flow.prepare, kind: 'call' });
|
|
379
|
+
}
|
|
380
|
+
else { /* none */ }
|
|
381
|
+
if (flow.present) {
|
|
382
|
+
c.then({ label: 'present: call ' + flow.present, kind: 'call' });
|
|
383
|
+
}
|
|
384
|
+
else { /* none */ }
|
|
385
|
+
c.fan('action', flow.performs.map((a) => [cut(a.when || a.action, 50), { label: `${a.action} (${a.effect}${a.confirm ? ', confirmed' : ''}): ${a.flow ? 'flow ' + a.flow : ''}${a.tool ? ' tool ' + a.tool : ''}`, kind: a.tool ? 'tool' : 'call' }]));
|
|
386
|
+
if (flow.project && flow.project.length) {
|
|
387
|
+
c.steps(flow.project);
|
|
388
|
+
}
|
|
389
|
+
else { /* no projection */ }
|
|
390
|
+
return c.done();
|
|
391
|
+
}
|
|
392
|
+
function stepsChart(list) { const c = builder(); c.steps(list); return c.done(); }
|
|
393
|
+
function facts(flow) {
|
|
394
|
+
const { flows, tools } = callsOf(flow);
|
|
395
|
+
const params = (flow.parameters || []).map((p) => `\`${p.name}\`${p.enum ? ' (' + p.enum.join(' / ') + ')' : ''}: ${p.description || ''}`);
|
|
396
|
+
const out = [
|
|
397
|
+
`**${flow.name || flow.id}** · \`${flow.id}\`${flow.version ? ' · version ' + flow.version : ''}${flow.primary ? ' · **entry point** (intent detection can start it' + (flow.inherited ? '; primary inherited from ' + flow.inherited : '') + ')' : ' · worker (called by other flows)'} · ${kindOf(flow).join(' + ')}${flow.scope ? ' · ' + flow.scope + ' scope' : ''}`,
|
|
398
|
+
'',
|
|
399
|
+
`> ${flow.description || ''}`,
|
|
400
|
+
'',
|
|
401
|
+
`- **Parameters:** ${params.length ? '\n - ' + params.join('\n - ') : 'none'}`,
|
|
402
|
+
`- **Calls:** ${flows.size ? [...flows].map(([id, cts]) => `${link(id)} (${[...cts].join(', ')})`).join(', ') : 'no other flow'}`,
|
|
403
|
+
`- **Tools:** ${tools.size ? [...tools].map((t) => '`' + t + '`').join(', ') : 'none'}`,
|
|
404
|
+
`- **Called by:** ${[...(flow.primary ? ['intent detection'] : []), ...(hostRun[flow.id] ? [hostRun[flow.id]] : []), ...[...(callers.get(flow.id) || new Map())].map(([id, hows]) => link(id) + ([...hows].some((h) => !CALL_TYPES.has(h)) ? ` (${[...hows].filter((h) => !CALL_TYPES.has(h)).join(', ')})` : ''))].join(', ') || 'nothing (unreachable)'}`,
|
|
405
|
+
];
|
|
406
|
+
if (flow.functions) {
|
|
407
|
+
out.push(`- **Functions:** ${Object.keys(flow.functions).map((n) => '`' + n + '`').join(', ')}`);
|
|
408
|
+
}
|
|
409
|
+
else { /* none */ }
|
|
410
|
+
if (flow.provides) {
|
|
411
|
+
out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), '', providerChart(flow.provides));
|
|
412
|
+
}
|
|
413
|
+
else { /* not a provider */ }
|
|
414
|
+
if (flow.form) {
|
|
415
|
+
out.push('', '### Form', '', formChart(flow.form));
|
|
416
|
+
}
|
|
417
|
+
else { /* not a form */ }
|
|
418
|
+
// A form the jsfe SDK compiled (the system set) carries its inputs as a variable; its steps are the engine's run-form loop.
|
|
419
|
+
const compiled = flow.metadata && flow.metadata.formProtocol && flow.variables && flow.variables.inputs && Array.isArray(flow.variables.inputs.value) ? flow.variables.inputs.value : null;
|
|
420
|
+
if (!flow.form && compiled && compiled.length) {
|
|
421
|
+
out.push('', '### Form inputs', '', formChart({ inputs: compiled }));
|
|
422
|
+
}
|
|
423
|
+
else { /* not a compiled form */ }
|
|
424
|
+
if (flow.performs) {
|
|
425
|
+
out.push('', '### Performs', '', ...flow.performs.map((a) => `- \`${a.action}\` (${a.effect}): ${a.reconcile && a.reconcile.policy ? cut(a.reconcile.policy, 300) : ''}`), '', performsChart(flow));
|
|
426
|
+
}
|
|
427
|
+
else { /* not a performer */ }
|
|
428
|
+
if (flow.call) {
|
|
429
|
+
out.push('', '### Call', '', `Runs ${link(flow.call.flow)} with its parameters.`);
|
|
430
|
+
}
|
|
431
|
+
else { /* not a call */ }
|
|
432
|
+
if (flow.wraps) {
|
|
433
|
+
out.push('', '### Wraps', '', `Runs ${link(flow.wraps)} with this set's wording.`);
|
|
434
|
+
}
|
|
435
|
+
else { /* not a wrapper */ }
|
|
436
|
+
if (flow.steps && flow.steps.length) {
|
|
437
|
+
out.push('', '### Steps', '', stepsChart(flow.steps));
|
|
438
|
+
}
|
|
439
|
+
else { /* no steps */ }
|
|
440
|
+
return out.join('\n');
|
|
441
|
+
}
|
|
442
|
+
function upsert(file, title, generated, skeleton) {
|
|
443
|
+
const block = `${START}\n${generated}\n${END}`;
|
|
444
|
+
const old = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
445
|
+
const next = old && old.includes(START) && old.includes(END)
|
|
446
|
+
? old.slice(0, old.indexOf(START)) + block + old.slice(old.indexOf(END) + END.length)
|
|
447
|
+
: `# ${title}\n\n${block}\n${skeleton ? '\n' + skeleton : ''}`;
|
|
448
|
+
if (next === old) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
else if (check) {
|
|
452
|
+
log(`STALE ${path.relative(root, file)}`);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
fs.writeFileSync(file, next);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
let failed = 0;
|
|
461
|
+
for (const set of SETS) {
|
|
462
|
+
current = set.key;
|
|
463
|
+
const dir = path.join(root, set.key), ids = new Set(set.flows.map((f) => f.id));
|
|
464
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
465
|
+
let changed = 0;
|
|
466
|
+
// ai-ar-ql's entry points carry a written part; elsewhere a flow's generated facts are its documentation.
|
|
467
|
+
const SKELETON = '## What it does\n\n_To be written._\n';
|
|
468
|
+
for (const f of set.flows) {
|
|
469
|
+
if (upsert(path.join(dir, f.id + '.md'), f.name || f.id, facts(f), set.skeleton && f.primary ? SKELETON : '')) {
|
|
470
|
+
changed++;
|
|
471
|
+
}
|
|
472
|
+
else { /* unchanged */ }
|
|
473
|
+
}
|
|
474
|
+
const entries = set.flows.filter((f) => f.primary), workers = set.flows.filter((f) => !f.primary);
|
|
475
|
+
// The README's graph shows what intent detection can start and the flows each entry point reaches directly.
|
|
476
|
+
const graph = ['flowchart LR'];
|
|
477
|
+
for (const f of entries) {
|
|
478
|
+
graph.push(` ${safe(f.id)}(["${esc(f.name || f.id)} · ${f.id}"])`);
|
|
479
|
+
for (const [t, hows] of callsOf(f).flows) {
|
|
480
|
+
const target = ids.has(t) ? `${safe(t)}["${esc(t)}"]` : `x_${safe(t)}[("${esc(t)} (${setOf.get(t) || 'not in this checkout'})")]`;
|
|
481
|
+
graph.push(` ${safe(f.id)} ${[...hows].some((h) => CALL_TYPES.has(h)) ? '-->' : '-.->'} ${target}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const table = (list) => ['| flow | kind | what it is |', '|---|---|---|', ...list.map((f) => `| [${f.id}](${f.id}.md) | ${kindOf(f).join(' + ')} | ${cut(f.description || '', 140).replace(/\|/g, '/')} |`)].join('\n');
|
|
485
|
+
const index = [`${set.flows.length} flows (${entries.length} entry points), ${set.tools.length} tools. The graph shows each entry point and the flows it reaches directly; a dotted arrow is a flow named rather than called (a hook, a prepare or present step, a default target). Each flow page charts its declarations: a provider (gate, source, facts, menu), a form (inputs in order, then its after steps), a performer (prepare, present, action) and any step list.`, '', '```mermaid', graph.join('\n'), '```', '', '## Entry points', '', entries.length ? table(entries) : 'None: intent detection starts no flow of this set directly.', '', '## Workers', '', table(workers)].join('\n');
|
|
486
|
+
const others = SETS.filter((s) => s !== set);
|
|
487
|
+
const readme = `## How to read these docs\n\nEach flow page has a generated part (facts and charts, rewritten from ${set.source})${set.skeleton ? ' and, for entry points, a written part' : ''}.${set.how ? ' ' + set.how : ''}${others.length ? ' The other sets: ' + others.map((s) => `[${s.key}](../${s.key}/README.md)`).join(', ') + '.' : ''}\n`;
|
|
488
|
+
if (upsert(path.join(dir, 'README.md'), set.title, index, readme)) {
|
|
489
|
+
changed++;
|
|
490
|
+
}
|
|
491
|
+
else { /* unchanged */ }
|
|
492
|
+
// A page whose flow is gone (renamed, deleted) is wrong about the flow set: it must go too.
|
|
493
|
+
const orphans = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && f !== 'README.md' && !ids.has(f.slice(0, -3)));
|
|
494
|
+
for (const o of orphans) {
|
|
495
|
+
log(`ORPHAN ${set.key}/${o}: no ${set.key} flow '${o.slice(0, -3)}'`);
|
|
496
|
+
}
|
|
497
|
+
if (check) {
|
|
498
|
+
log(changed || orphans.length ? `${path.relative(process.cwd(), dir) || set.key}: ${changed} stale, ${orphans.length} orphan: run the generator` : `${path.relative(process.cwd(), dir) || set.key}: ${set.flows.length} flow docs + README current`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
log(`${path.relative(process.cwd(), dir) || set.key}: ${set.flows.length} flow docs + README, ${changed} written${orphans.length ? `; ${orphans.length} orphan page(s) to delete` : ''}`);
|
|
502
|
+
}
|
|
503
|
+
failed += check ? changed + orphans.length : orphans.length;
|
|
504
|
+
}
|
|
505
|
+
// Links are checked once every set's pages exist: a page links into the other sets' folders.
|
|
506
|
+
for (const set of SETS) {
|
|
507
|
+
const dir = path.join(root, set.key);
|
|
508
|
+
const broken = [];
|
|
509
|
+
for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.md'))) {
|
|
510
|
+
for (const m of fs.readFileSync(path.join(dir, f), 'utf8').matchAll(/\]\(((?:\.\.\/[a-z0-9-]+\/)?[a-z0-9:-]+\.md)(?:#[^)]*)?\)/gi)) {
|
|
511
|
+
if (!fs.existsSync(path.join(dir, m[1]))) {
|
|
512
|
+
broken.push(`${set.key}/${f} → ${m[1]}`);
|
|
513
|
+
}
|
|
514
|
+
else { /* exists */ }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const b of broken) {
|
|
518
|
+
log(`BROKEN ${b}`);
|
|
519
|
+
}
|
|
520
|
+
failed += broken.length;
|
|
521
|
+
}
|
|
522
|
+
return { failed };
|
|
523
|
+
}
|
|
@@ -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":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAueD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3qBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAo2B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -40,6 +40,13 @@ const optionalIncomplete = (id, condition, form, reason) => WHEN(id, `(${conditi
|
|
|
40
40
|
export const textValue = (text) => (String(text).includes('{{') ? String(text) : JSON.stringify(String(text)));
|
|
41
41
|
// Keys for a numbered menu: 1..9 without 8, the language key (owner rule 2026-10-19).
|
|
42
42
|
const KEYS_JS = "(function (n) { var k = []; for (var i = 1; k.length < n && i <= 9; i++) { if (i !== 8) { k.push(String(i)); } } return k; })";
|
|
43
|
+
// Month names a card expiration may be said with (English, Spanish), longest first so 'sept' is not read as 'sep'.
|
|
44
|
+
const MONTH_NAMES = [['september', 9], ['septiembre', 9], ['setiembre', 9], ['february', 2], ['febrero', 2], ['november', 11], ['noviembre', 11],
|
|
45
|
+
['december', 12], ['diciembre', 12], ['january', 1], ['octubre', 10], ['october', 10], ['august', 8], ['agosto', 8], ['enero', 1], ['marzo', 3],
|
|
46
|
+
['march', 3], ['abril', 4], ['april', 4], ['junio', 6], ['julio', 7], ['mayo', 5], ['june', 6], ['july', 7], ['sept', 9], ['jan', 1], ['ene', 1],
|
|
47
|
+
['feb', 2], ['mar', 3], ['apr', 4], ['abr', 4], ['may', 5], ['jun', 6], ['jul', 7], ['aug', 8], ['ago', 8], ['sep', 9], ['oct', 10], ['nov', 11],
|
|
48
|
+
['dec', 12], ['dic', 12]];
|
|
49
|
+
const MONTH_RE = MONTH_NAMES.map(([n]) => n).join('|');
|
|
43
50
|
// ── field presets (decision 2A): ONE library, read only by collect-input ─────────────────────────────────────────
|
|
44
51
|
// A form uses a preset by name ({ name, field }); keys it gives override the preset's (e.g. a tenant's account pattern).
|
|
45
52
|
export const FIELD_PRESETS = {
|
|
@@ -75,8 +82,9 @@ export const FIELD_PRESETS = {
|
|
|
75
82
|
retry: 'Please say or enter the full card number.', retry_es: 'Por favor diga o ingrese el número completo de su tarjeta.',
|
|
76
83
|
retry_text: 'Please type the full card number.', retry_text_es: 'Por favor escriba el número completo de su tarjeta.' },
|
|
77
84
|
card_expiration: { label: 'expiration date', label_es: 'fecha de vencimiento',
|
|
78
|
-
// MM0YY: a zero said between month and year ("020 28", prod 2026-10-25 14:10, was taken for the billing ZIP)
|
|
79
|
-
|
|
85
|
+
// MM0YY: a zero said between month and year ("020 28", prod 2026-10-25 14:10, was taken for the billing ZIP).
|
|
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 },
|
|
80
88
|
validator: 'validate-card-expiration',
|
|
81
89
|
question: 'What is the expiration date, month and year?', question_es: '¿Cuál es la fecha de vencimiento, mes y año?',
|
|
82
90
|
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.',
|
|
@@ -635,9 +643,9 @@ const validators = [
|
|
|
635
643
|
parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }],
|
|
636
644
|
steps: [SIDE('vcn_check', "(cargo.inputCheck = validateCardNumber(value) ? { ok: true, value: String(value).replace(/\\D/g, '') } : { ok: false, message: \"That card number doesn't look right.\", message_es: 'Ese número de tarjeta no parece correcto.' }, true)")] },
|
|
637
645
|
{ id: 'validate-card-expiration', name: 'ValidateCardExpiration', version: '1.0.0', scope: 'isolated',
|
|
638
|
-
description: 'Validator: month and year, a real month, not expired (validateExpiration). Normalizes to MMYY. Params: input, value.',
|
|
646
|
+
description: 'Validator: month (a number or its English or Spanish name) and year, a real month, not expired (validateExpiration). Normalizes to MMYY. Params: input, value.',
|
|
639
647
|
parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }],
|
|
640
|
-
steps: [SIDE('vce_check', "(function (v) { var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')) || /^(0[1-9])0(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
|
|
648
|
+
steps: [SIDE('vce_check', "(function (v) { var names = " + JSON.stringify(MONTH_NAMES.map(([n]) => n)) + "; var nums = " + JSON.stringify(MONTH_NAMES.map(([, k]) => k)) + "; var w = new RegExp('\\\\b(' + names.join('|') + ')\\\\b', 'i').exec(String(v)); if (w) { v = String(v).replace(w[0], ' ' + nums[names.indexOf(w[1].toLowerCase())] + ' '); } var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')) || /^(0[1-9])0(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
|
|
641
649
|
{ id: 'validate-card-cvv', name: 'ValidateCardCvv', version: '1.0.0', scope: 'isolated',
|
|
642
650
|
description: 'Validator: the security code has the length of its card brand, 4 digits for American Express (34, 37) and 3 for every other brand; with no card number yet, 3 or 4. Params: input, value, values.',
|
|
643
651
|
parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }, { name: 'values', type: 'object', description: "the form's values, for the card number" }],
|
package/dist/helpers/answer.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function isAmountAnswer(input: any): boolean;
|
|
|
31
31
|
* answer was taken, so "pagar cuenta 50, ciento treinta y cuatro…" paid $50, "$10 con 18 centavos" paid $10.00 and an
|
|
32
32
|
* offer read back with "terminada en 2 4 8 6" paid $2. Numbers that name an account, card, code or ZIP, and digits spoken
|
|
33
33
|
* one by one, are not amounts; two different remaining numbers are ambiguous unless exactly one is marked as money ($,
|
|
34
|
-
* dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50. Amounts said only in words are NaN (not parsed yet).
|
|
34
|
+
* dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50, "157 95" is $157.95. Amounts said only in words are NaN (not parsed yet).
|
|
35
35
|
*/
|
|
36
36
|
export function amountInAnswer(input: any): number;
|
|
37
37
|
/** Internal numeric evidence shared by the opt-in money reader; legacy results stay unchanged. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;
|
|
1
|
+
{"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAqBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAoCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AAxcD,qCAKwE"}
|
package/dist/helpers/answer.js
CHANGED
|
@@ -330,12 +330,16 @@ const DOLLARS_AND_CENTS = /(\d+)\s*(?:dollars?|d[oó]lar(?:es)?|bucks?)?\s*(?:an
|
|
|
330
330
|
// cents is allowed ("171 con un 06").
|
|
331
331
|
const CENTS_JOINERS = ['con', 'and']; // 'and' too (owner 2026-10-18: "87 and 21" = $87.21)
|
|
332
332
|
const DOLLARS_JOINED_CENTS = /(\$\s*)?(\d+)(\s*(?:dollars?|d[oó]lar(?:es)?|bucks?))?\s*(and|con|y)\s*(?:un\s+)?(\d{1,2})\b(?!\s*\d)/gi;
|
|
333
|
+
// Dollars and two cent digits said with no word between them, as speech-to-text writes "ciento cincuenta y siete noventa y
|
|
334
|
+
// cinco": "157 95", "$157 95", "1 50" (owner 2026-10-29; prod 10-28 callers re-asked). Never after a decimal or before more
|
|
335
|
+
// digits, and never two-digit dollars: "95 52" is four digits, a card's last four read in pairs.
|
|
336
|
+
const DOLLARS_SPACE_CENTS = /(?<![\d.,])(\$\s*)?(\d|\d{3,})\s+(\d{2})(?![\d.,]|\s*(?:dollars?|d[oó]lar(?:es)?|bucks?|cents?|centavos?)\b)/gi;
|
|
333
337
|
/**
|
|
334
338
|
* The amount an answer names, or NaN when it names none or is ambiguous. Prod replay 2026-10-14: the first number in the
|
|
335
339
|
* answer was taken, so "pagar cuenta 50, ciento treinta y cuatro…" paid $50, "$10 con 18 centavos" paid $10.00 and an
|
|
336
340
|
* offer read back with "terminada en 2 4 8 6" paid $2. Numbers that name an account, card, code or ZIP, and digits spoken
|
|
337
341
|
* one by one, are not amounts; two different remaining numbers are ambiguous unless exactly one is marked as money ($,
|
|
338
|
-
* dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50. Amounts said only in words are NaN (not parsed yet).
|
|
342
|
+
* dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50, "157 95" is $157.95. Amounts said only in words are NaN (not parsed yet).
|
|
339
343
|
*/
|
|
340
344
|
export function amountInAnswer(input) { return numericAmountReading(input).value; }
|
|
341
345
|
/** Internal numeric evidence shared by the opt-in money reader; legacy results stay unchanged. */
|
|
@@ -358,7 +362,8 @@ export function numericAmountReading(input) {
|
|
|
358
362
|
else {
|
|
359
363
|
// no dollars-and-cents phrase
|
|
360
364
|
}
|
|
361
|
-
t = t.replace(IDENTIFIER_NUMBER, ' ').replace(SPELLED_DIGITS, ' ').replace(DOLLARS_JOINED_CENTS, (m, sign, d, word, joiner, c) => (sign || word || (CENTS_JOINERS.includes(joiner.toLowerCase()) && c.length === 2) ? ` $${d}.${c.padStart(2, '0')} ` : m))
|
|
365
|
+
t = t.replace(IDENTIFIER_NUMBER, ' ').replace(SPELLED_DIGITS, ' ').replace(DOLLARS_JOINED_CENTS, (m, sign, d, word, joiner, c) => (sign || word || (CENTS_JOINERS.includes(joiner.toLowerCase()) && c.length === 2) ? ` $${d}.${c.padStart(2, '0')} ` : m))
|
|
366
|
+
.replace(DOLLARS_SPACE_CENTS, (m, sign, d, c) => ` $${d}.${c} `);
|
|
362
367
|
const found = [...t.matchAll(/(\$\s*)?(\d+(?:\.\d{1,2})?)(\s*(?:dollars?|d[oó]lar(?:es)?|bucks?|cents?|centavos?)\b)?/gi)]
|
|
363
368
|
.map((m) => ({ v: parseFloat(m[2]), marked: !!(m[1] || m[3] || m[2].includes('.')), cents: !m[1] && /cent/i.test(m[3] || '') }));
|
|
364
369
|
const values = [...new Set(found.map((f) => f.v))];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/helpers/money.js"],"names":[],"mappings":"AAIA,gHAAgH;AAChH,wDAGC;
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/helpers/money.js"],"names":[],"mappings":"AAIA,gHAAgH;AAChH,wDAGC;AAkBD;8FAC8F;AAC9F,oEAIC;AACD,+FAA+F;AAC/F,0DAEC;AACD;oGACoG;AACpG,4DAGC;AAWD;;;GAGG;AACH,+DAYC;AACD,4FAA4F;AAC5F,iEAAsG"}
|
package/dist/helpers/money.js
CHANGED
|
@@ -8,8 +8,9 @@ export function validateMoneyOptions(options) {
|
|
|
8
8
|
return options;
|
|
9
9
|
}
|
|
10
10
|
// This is an input grammar, not a conversation classifier. Anything outside one complete
|
|
11
|
-
// monetary answer belongs to DISPATCH, including refusals, corrections with context and reports.
|
|
12
|
-
|
|
11
|
+
// monetary answer belongs to DISPATCH, including refusals, corrections with context and reports. "157 95" is dollars and
|
|
12
|
+
// cents said with no word between them (owner 2026-10-29); "95 52" is not (a card's last four read in pairs).
|
|
13
|
+
const NUMERIC_MONEY = /^(?:\$\s*)?(?:\d{1,3}(?:[,.]\d{3})+|\d+)(?:[.,]\d{1,2})?(?:\s*(?:dollars?|d[oó]lar(?:es)?|bucks?))?(?:\s*(?:and|con|y)\s*(?:un\s*)?\d{1,2}(?:\s*(?:cents?|centavos?))?)?$|^\d{1,2}\s*(?:cents?|centavos?)$|^(?:\$\s*)?(?:\d|\d{3,})\s+\d{2}$/i;
|
|
13
14
|
const PAYMENT_PREFIX = /^(?:(?:i (?:want to|would like to|can|am able to|will) |i['’]d like to )?pay|(?:quiero |quisiera |puedo |s[ií] puedo |voy a )?(?:pagar|abonar)|(?:no,?\s+)?make it)\s*(?=\$|\d|[a-záéíóúüñ])/i;
|
|
14
15
|
const ACCOUNT_PREFIX = /^(?:(?:sub[ -]?account|subcuenta|cuenta)\s+\d{1,8}\s*,\s*)/i;
|
|
15
16
|
const DIGIT_WORD = '(?:zero|one|two|three|four|five|six|seven|eight|nine|cero|uno|dos|tres|cuatro|cinco|seis|siete|ocho|nueve)';
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:07545e99b09fda4ba770a9732d47399cee27e0ec746b824efa9e3f4124e74bb6";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ron Pinkas",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./dist/helpers/index.d.ts",
|
|
28
28
|
"import": "./dist/helpers/index.js"
|
|
29
29
|
},
|
|
30
|
+
"./docs": {
|
|
31
|
+
"types": "./dist/docs/flow-docs.d.ts",
|
|
32
|
+
"import": "./dist/docs/flow-docs.js"
|
|
33
|
+
},
|
|
30
34
|
"./forms": {
|
|
31
35
|
"types": "./dist/forms/index.d.ts",
|
|
32
36
|
"import": "./dist/forms/index.js"
|