jsfe 1.14.2 → 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 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-flows.md)** and **[Shopify Flows](docs/shopify-flows.md)** - the bundled flow and tool libraries in `flows/`, step by step
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`.
@@ -354,7 +354,10 @@ The host runs either, or both as a hedge, and replies with the canonical JSON: `
354
354
  stage 1, `{ "parameters": { … } }` for stage 2. `jsfe/host` exports `buildFlowStage`, `buildParameterStage`
355
355
  and the decoders (`decodeFlowLlm`, `decodeFlowSystemOne`, `decodeParametersLlm`, `decodeParametersSystemOne`,
356
356
  `isNearMiss`), so a host never interprets an answer itself. Only primaries are ever activated by detection;
357
- a stage-2 failure starts the flow without its parameters (it asks for them). The default, `'single-call'`,
357
+ a stage-2 failure starts the flow without its parameters (it asks for them). Stage 1's System One request
358
+ is one `choice` over the primaries (`route`) and one `noul` (`any`): does the message start any of them; the
359
+ routed flow starts when `any` reaches 0.5, else `"None"`. Both executors are told that a question starts a flow
360
+ only when it asks for the flow's action, never because it mentions its subject. The default, `'single-call'`,
358
361
  is the original single call.
359
362
 
360
363
  **The reply ranker (contextual DISPATCH, `onResponse: "continue"`):** when a flow's question gets a reply its
@@ -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":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAueD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAnqBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA41B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
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"}
@@ -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
- profile: { kind: 'pattern', regex: '\\b(0?[1-9]|1[0-2])\\s*(?:/|-|\\s|de)?\\s*(?:20)?(\\d{2})\\b|\\b0[1-9]0\\s+(?:20)?\\d{2}\\b' }, digits: { min: 3, max: 6 },
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" }],
@@ -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;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AAncD,qCAKwE"}
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"}
@@ -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;AAiBD;8FAC8F;AAC9F,oEAIC;AACD,+FAA+F;AAC/F,0DAEC;AACD;oGACoG;AACpG,4DAGC;AAWD;;;GAGG;AACH,+DAYC;AACD,4FAA4F;AAC5F,iEAAsG"}
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"}
@@ -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
- 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?)$/i;
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)';
@@ -23,8 +23,6 @@
23
23
  import type { FlowParameterDefinition } from '../index.js';
24
24
  /** A noul at or above this is a yes. */
25
25
  export declare const STAGE_YES_THRESHOLD = 0.5;
26
- /** Stage 1, System One: two top flows that together reach this pick the higher one (prod 10-17). */
27
- export declare const STAGE_PAIR_SUM = 0.7;
28
26
  export interface StageTurn {
29
27
  role: 'user' | 'assistant';
30
28
  content: string;
@@ -52,9 +50,9 @@ export interface StageSystemOne<I> {
52
50
  questions: Record<string, unknown>;
53
51
  index: I[];
54
52
  }
53
+ /** Stage 1, System One: `route` is the choice among the flows, `any` whether the message starts one at all. */
55
54
  export interface FlowQuestionRef {
56
- id: string;
57
- flow: number;
55
+ id: 'route' | 'any';
58
56
  }
59
57
  export type ParameterQuestionRef = {
60
58
  id: string;
@@ -99,26 +97,34 @@ export type StageSystemOneAnswers = Record<string, {
99
97
  } | undefined> | undefined;
100
98
  /** The flow's own parameters, never asked of the user. */
101
99
  export declare const isInternalParameter: (p: StageParameter | FlowParameterDefinition) => boolean;
102
- /** Stage 1's instructions for both executors. `flows` are the primaries offered. */
100
+ /**
101
+ * Stage 1's instructions for both executors. `flows` are the primaries offered.
102
+ *
103
+ * System One gets ONE request with two questions over the same list: `route`, a `choice` among the flows (each
104
+ * option's criterion is its description, so every flow is weighed against the others), and `any`, a `noul`
105
+ * asking whether the message starts any of them. TypeSafe documents that a "none of the above" option may not
106
+ * work well and advises the companion noul.
107
+ */
103
108
  export declare function buildFlowStage(input: string, conversation: StageTurn[], flows: StageFlow[]): FlowStageRequest;
104
109
  /** A text model's stage-1 answer as the canonical reply; throws when it names no offered flow. */
105
110
  export declare function decodeFlowLlm(text: string, stage: FlowStageRequest): {
106
111
  flowName: string;
107
112
  };
108
113
  /**
109
- * System One's stage-1 answers as the canonical reply, with the top three scores (for logs and a host's hedge
110
- * policy) and `pair` when two close candidates picked the higher. Throws if any question is unanswered.
114
+ * System One's stage-1 answers as the canonical reply. `any` at or above the threshold starts the flow `route`
115
+ * chose; below it, "None". `scores` are the top three flows by the chance that each is the one started,
116
+ * any × its route probability (for logs and the host's near-miss hedge); `any` is logged with them. Throws when
117
+ * either answer is missing or names no offered flow.
111
118
  */
112
- export declare function decodeFlowSystemOne(answers: StageSystemOneAnswers, stage: FlowStageRequest, { threshold, pairSum }?: {
119
+ export declare function decodeFlowSystemOne(answers: StageSystemOneAnswers, stage: FlowStageRequest, { threshold }?: {
113
120
  threshold?: number | undefined;
114
- pairSum?: number | undefined;
115
121
  }): {
116
122
  flowName: string;
117
123
  scores: {
118
124
  name: string;
119
125
  p: number;
120
126
  }[];
121
- pair: boolean;
127
+ any: number;
122
128
  };
123
129
  /**
124
130
  * A System One "None" that is a near miss: the chance that at least one of its two top flows applies,
@@ -1 +1 @@
1
- {"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"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"}
@@ -22,11 +22,11 @@
22
22
  */
23
23
  /** A noul at or above this is a yes. */
24
24
  export const STAGE_YES_THRESHOLD = 0.5;
25
- /** Stage 1, System One: two top flows that together reach this pick the higher one (prod 10-17). */
26
- export const STAGE_PAIR_SUM = 0.7;
27
25
  // ── the rules: written once, rendered for both executors ─────────────────────────────────────────────
28
26
  const FLOW_TRUE = "The latest message, read with the conversation, is what the workflow's description says should start it: the user asks for its action or information, or raises the problem it exists to handle. Also when it accepts the assistant's offer in the conversation to start or resume it (assistant: \"Shall I try the payment again?\" → user: \"yes\").";
29
27
  const FLOW_FALSE = "It only shares a topic with the workflow, is not what the workflow's description covers, or is a greeting, an acknowledgement or a fragment that asks for nothing.";
28
+ /** A question usually asks for information the assistant gives itself; it is weighed before it is taken as a trigger. */
29
+ const QUESTION_RULE = "A question usually asks for information the assistant can give itself. Consider it carefully: it starts a workflow only when it asks for that workflow's action or raises the problem it exists to handle, never because it mentions the workflow's subject (\"can I pay with this card at a store?\" asks how the card is used, not to make a payment).";
30
30
  const PARAMETER_RULES = [
31
31
  'Give a value only when the user gave it, in the latest message or earlier in the conversation. Never guess.',
32
32
  "Write it as the parameter's rule says; otherwise the value alone, in the user's words (never the whole sentence around it).",
@@ -49,33 +49,45 @@ const stateOf = (conversation, input, instructions) => ({
49
49
  });
50
50
  const jsonTail = (schema) => `\n\n<json-schema>\n${JSON.stringify(schema)}\n</json-schema>\n\nRespond ONLY with valid JSON matching the schema. No other text.`;
51
51
  // ── stage 1 ───────────────────────────────────────────────────────────────────────────────────────────
52
- /** Stage 1's instructions for both executors. `flows` are the primaries offered. */
52
+ /**
53
+ * Stage 1's instructions for both executors. `flows` are the primaries offered.
54
+ *
55
+ * System One gets ONE request with two questions over the same list: `route`, a `choice` among the flows (each
56
+ * option's criterion is its description, so every flow is weighed against the others), and `any`, a `noul`
57
+ * asking whether the message starts any of them. TypeSafe documents that a "none of the above" option may not
58
+ * work well and advises the companion noul.
59
+ */
53
60
  export function buildFlowStage(input, conversation, flows) {
54
61
  const names = flows.map(f => f.name);
55
62
  const schema = {
56
63
  type: 'object', additionalProperties: false, required: ['flowName'],
57
64
  properties: { flowName: { type: 'string', enum: [...names, 'None'] } },
58
65
  };
66
+ const list = flows.map(f => `- ${f.name}: ${f.description}`);
59
67
  const system = [
60
68
  'Decide which workflow, if any, the latest message starts.',
61
69
  `A workflow is chosen when: ${FLOW_TRUE}`,
62
70
  `"None" when: ${FLOW_FALSE}`,
71
+ QUESTION_RULE,
63
72
  'Answer with the exact workflow name, or "None".',
64
73
  '',
65
74
  '<workflows>',
66
- ...flows.map(f => `- ${f.name}: ${f.description}`),
75
+ ...list,
67
76
  '</workflows>',
68
77
  ].join('\n') + jsonTail(schema);
69
- const questions = {};
70
- const index = [];
71
- flows.forEach((f, i) => {
72
- const id = `f${i}`;
73
- questions[id] = {
74
- type: 'noul', criteria: { true: FLOW_TRUE, false: FLOW_FALSE },
75
- instructions: { flow: { name: f.name, description: f.description }, question: 'Should the latest message start the workflow in `flow`?' },
76
- };
77
- index.push({ id, flow: i });
78
- });
78
+ const workflows = Object.fromEntries(flows.map(f => [f.name, f.description]));
79
+ const questions = {
80
+ any: {
81
+ type: 'noul',
82
+ criteria: { true: `${FLOW_TRUE} The workflow is one of \`workflows\`.`, false: `${FLOW_FALSE} ${QUESTION_RULE}` },
83
+ instructions: { workflows, question: 'Should the latest message start one of the workflows in `workflows`?' },
84
+ },
85
+ };
86
+ questions.route = {
87
+ type: 'choice', criteria: workflows,
88
+ instructions: 'Which workflow does the latest message, read with the conversation, come closest to starting?',
89
+ };
90
+ const index = [{ id: 'any' }, { id: 'route' }];
79
91
  return {
80
92
  task: 'detect_flow', stage: 1, input, conversation, flows,
81
93
  llm: { system, user: conversationText(conversation, input), schema },
@@ -97,23 +109,27 @@ export function decodeFlowLlm(text, stage) {
97
109
  }
98
110
  }
99
111
  /**
100
- * System One's stage-1 answers as the canonical reply, with the top three scores (for logs and a host's hedge
101
- * policy) and `pair` when two close candidates picked the higher. Throws if any question is unanswered.
112
+ * System One's stage-1 answers as the canonical reply. `any` at or above the threshold starts the flow `route`
113
+ * chose; below it, "None". `scores` are the top three flows by the chance that each is the one started,
114
+ * any × its route probability (for logs and the host's near-miss hedge); `any` is logged with them. Throws when
115
+ * either answer is missing or names no offered flow.
102
116
  */
103
- export function decodeFlowSystemOne(answers, stage, { threshold = STAGE_YES_THRESHOLD, pairSum = STAGE_PAIR_SUM } = {}) {
104
- const p = answered(answers, stage.systemOne.index);
105
- const ranked = stage.systemOne.index.map(q => ({ name: stage.flows[q.flow].name, p: p.get(q.id) })).sort((a, b) => b.p - a.p);
117
+ export function decodeFlowSystemOne(answers, stage, { threshold = STAGE_YES_THRESHOLD } = {}) {
118
+ const any = Number(answers?.any?.noul);
119
+ if (!Number.isFinite(any))
120
+ throw new Error('System One: question any unanswered');
121
+ const names = stage.flows.map(f => f.name);
122
+ const route = answers?.route;
123
+ const raw = route?.probabilities;
124
+ if (!raw || typeof raw !== 'object')
125
+ throw new Error('System One: question route unanswered');
126
+ if (!names.includes(String(route?.choice)))
127
+ throw new Error(`System One: route chose "${String(route?.choice).slice(0, 60)}", not an offered workflow`);
128
+ const probabilities = Object.fromEntries(names.map(n => [n, Number(raw[n]) || 0]));
129
+ const ranked = names.map(n => ({ name: n, p: any * probabilities[n] })).sort((a, b) => b.p - a.p);
106
130
  const scores = ranked.slice(0, 3);
107
- const [a, b] = ranked;
108
- if (a && a.p >= threshold) {
109
- return { flowName: a.name, scores, pair: false };
110
- }
111
- else if (a && b && a.p + b.p >= pairSum) {
112
- return { flowName: a.name, scores, pair: true };
113
- }
114
- else {
115
- return { flowName: 'None', scores, pair: false };
116
- }
131
+ const flowName = any >= threshold && ranked[0] ? ranked[0].name : 'None';
132
+ return { flowName, scores, any };
117
133
  }
118
134
  /**
119
135
  * A System One "None" that is a near miss: the chance that at least one of its two top flows applies,
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:9fca8fe719c5610d1260ece2cc393107d549667095f71bcaa62427a894f0ba7b";
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.14.2",
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"