jsfe 1.15.9 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +1 -0
  2. package/dist/docs/flow-docs.d.ts.map +1 -1
  3. package/dist/docs/flow-docs.js +4 -2
  4. package/dist/forms/address-flows.js +1 -1
  5. package/dist/forms/continuation-compat.d.ts +5 -0
  6. package/dist/forms/continuation-compat.d.ts.map +1 -0
  7. package/dist/forms/continuation-compat.js +45 -0
  8. package/dist/forms/contracts.d.ts.map +1 -1
  9. package/dist/forms/contracts.js +6 -4
  10. package/dist/forms/declarations.d.ts +3 -0
  11. package/dist/forms/declarations.d.ts.map +1 -1
  12. package/dist/forms/declarations.js +26 -4
  13. package/dist/forms/flows.d.ts.map +1 -1
  14. package/dist/forms/flows.js +12 -6
  15. package/dist/forms/identity-flows.d.ts +3 -0
  16. package/dist/forms/identity-flows.d.ts.map +1 -1
  17. package/dist/forms/identity-flows.js +53 -7
  18. package/dist/forms/index.d.ts +1 -0
  19. package/dist/forms/index.js +1 -0
  20. package/dist/helpers/address.d.ts +1 -0
  21. package/dist/helpers/address.d.ts.map +1 -1
  22. package/dist/helpers/address.js +4 -1
  23. package/dist/helpers/form-actions.js +1 -1
  24. package/dist/helpers/index.d.ts +2 -1
  25. package/dist/helpers/index.d.ts.map +1 -1
  26. package/dist/helpers/index.js +2 -2
  27. package/dist/helpers/otp-check.d.ts +9 -3
  28. package/dist/helpers/otp-check.d.ts.map +1 -1
  29. package/dist/helpers/otp-check.js +20 -10
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +7 -1
  33. package/dist/runtime-identity.js +1 -1
  34. package/docs/architecture-guide.md +366 -0
  35. package/docs/copilot-reference.md +2 -4
  36. package/docs/development-release.md +7 -1
  37. package/package.json +2 -1
@@ -0,0 +1,366 @@
1
+ # JSFE flows architecture guide
2
+
3
+ This guide is for developers new to JSFE. It explains the ideas behind the engine, the building blocks a flow set is
4
+ made of, what the caller sees and hears, and how a conversation can feel free and natural while the transactions in it
5
+ stay exact and safe. It introduces concepts; the [flow author reference](copilot-reference.md) is the precise
6
+ specification, and the [README](../README.md) is the API reference.
7
+
8
+ The examples use a fictional utility company. Every name and number in them is invented.
9
+
10
+ ## 1. The problem: two kinds of software in one conversation
11
+
12
+ A caller who phones a utility might say any of these in one call:
13
+
14
+ - "How much do I owe?"
15
+ - "I want to pay 80 dollars with my card ending in 4242."
16
+ - "Wait, make it 60."
17
+ - "When is it due, by the way?"
18
+ - "Actually, can I talk to a person?"
19
+
20
+ Two very different kinds of software have to serve that call.
21
+
22
+ **A conversational AI** understands people. It copes with any wording, any language, answers out of order, changes of
23
+ mind and questions in the middle of a task. But it guesses, and a guess is never acceptable when money moves.
24
+
25
+ **A transactional script** is exact. It verifies the caller, collects every required field, reads the amount back,
26
+ asks for a clear YES and charges the card once. But a classic script is rigid: one question at a time, one expected
27
+ answer, and anything else is "Sorry, I didn't understand."
28
+
29
+ JSFE joins the two. **The flow owns the transaction; the AI helps only with understanding language, and every AI
30
+ decision is a choice from a short closed list that the flow then checks.** The AI never writes a value into a
31
+ payment, never confirms a charge and never decides what the business does.
32
+
33
+ ## 2. The big picture
34
+
35
+ ```mermaid
36
+ flowchart LR
37
+ U([Caller: voice, SMS, chat]) --> H[Host application]
38
+ H -->|updateActivity| E[JSFE engine]
39
+ E -->|aiCallback: closed questions| AI[(Models: classifier and LLM)]
40
+ E -->|CALL-TOOL| T[(Business APIs)]
41
+ E -->|reply text, session| H
42
+ H -->|no flow serves the turn| G[Host AI answer: RAG, guru]
43
+ ```
44
+
45
+ - **The host** is your application: it owns the channels (phone, text, web), credentials, storage, model
46
+ configuration and turn order. It calls `engine.updateActivity(...)` once per caller message and saves the session the
47
+ engine returns.
48
+ - **The engine** runs flows. It decides whether the message belongs to a flow in progress, starts a new flow, or hands
49
+ the turn back to the host.
50
+ - **Models** are reached only through the host's `aiCallback`. The engine builds every question it asks a model, and
51
+ decodes every answer, so a host never interprets a model's output itself.
52
+ - **Tools** are the only way a flow touches the outside world (charge a card, send a text, open a ticket).
53
+ - **The host's own AI** answers whatever no flow serves: a general question, small talk, a product question answered
54
+ from documents.
55
+
56
+ ## 3. Building blocks
57
+
58
+ ### Flow sets and tools
59
+
60
+ A flow set is a JSON array of flow definitions (`tenant.flows`) with a matching JSON array of tool definitions
61
+ (`tenant.tools`). A host usually composes several sets: the **system** set every tenant shares (identity checks, field
62
+ collectors, retries, handoffs), optional product sets (for example a store integration), and the **tenant** set with
63
+ that business's own flows. Flows reference each other by id, so a tenant set reuses shared workers instead of copying
64
+ them.
65
+
66
+ ### Steps
67
+
68
+ A flow is made of steps. Most authors need only a handful:
69
+
70
+ | Step | What it does |
71
+ | --- | --- |
72
+ | `SAY` | Queue text for the caller. |
73
+ | `SAY-GET` | Say the queued text and wait for the caller's answer. |
74
+ | `SET` | Compute a value (a JavaScript expression) into a variable. |
75
+ | `CASE` / `SWITCH` | Branch on a condition or a value. |
76
+ | `FLOW` | Run another flow: `call` (and come back), `replace` or `reboot`. |
77
+ | `CALL-TOOL` | Call a registered tool, with an `onFail` route. |
78
+ | `END` | Finish this flow and return to the one that called it. |
79
+ | `RETURN` | Finish every flow and hand a reply (or the turn) to the host. |
80
+ | `DISPATCH` | Finish every flow and send this message to intent detection, as if it were the first message. |
81
+
82
+ ### The four authoring concepts
83
+
84
+ Steps are the machine code. Authors mostly write **declarations**, and JSFE generates the steps.
85
+
86
+ | Concept | You declare | JSFE gives you |
87
+ | --- | --- | --- |
88
+ | **Form** | the inputs a task needs | collection in any order, validation, corrections, resume after a detour |
89
+ | **Provider** | the facts a flow can answer and where they come from | answers to exactly the fact asked, behind an identity gate |
90
+ | **Performer** | an action: its effect, gate, tool, confirmation and outcomes | a guarded, confirmed, single execution |
91
+ | **Worker** | a few explicit steps | a small reusable task (a calculation, a lookup, a handoff) |
92
+
93
+ **Forms: any order, one question at a time.** A form lists its inputs once. The engine loops: find the first missing
94
+ input, ask it, store whatever the answer actually contained, repeat. Because of that loop, a caller who says
95
+ everything at once, answers a later question early, corrects an earlier answer, or comes back after a detour needs no
96
+ special code:
97
+
98
+ ```json
99
+ {
100
+ "declarationVersion": 2, "id": "pay-bill", "primary": true,
101
+ "description": "The caller wants to pay their bill now. Not: a question about a past payment.",
102
+ "form": {
103
+ "inputs": [
104
+ { "name": "account", "field": "account_number" },
105
+ { "name": "amount", "profile": { "kind": "amount", "min": 1, "money": { "currency": "USD", "cardinals": true, "intent": "payment" } },
106
+ "question": "How much would you like to pay?", "question_es": "¿Cuánto desea pagar?" },
107
+ { "name": "card", "form": "collect-credit-card" }
108
+ ]
109
+ }
110
+ }
111
+ ```
112
+
113
+ Forms nest: a payment form contains a card form, which contains card-number, expiration and security-code fields. An
114
+ answer may fill an input of any active form in the chain, so "use the card ending in 4242" is understood while the
115
+ payment is still asking for the amount.
116
+
117
+ **Providers: answer what was asked.** A provider declares a catalog of facts (balance, due date, last payment), each
118
+ with its source path, its wording and the identity gate that protects it. JSFE derives the rest from that one catalog:
119
+ the `asked` parameter intent detection fills, the words that recognize each fact in the middle of another task, and the
120
+ menu offered after an answer. "When is it due?" gets the due date, not a summary of the whole account.
121
+
122
+ **Performers: actions with guard rails.** A performer is a form plus a declared action:
123
+
124
+ ```json
125
+ "performs": [{
126
+ "action": "bill_payment", "effect": "charge", "subject": "form.data.account",
127
+ "gate": { "flow": "verify-caller", "verified": ["cargo.verified"] },
128
+ "flow": "payment-execute", "confirm": { "exact": true },
129
+ "outcomes": { "ok": "resolved", "failed": "unresolved", "unknown": "unresolved" }
130
+ }]
131
+ ```
132
+
133
+ From that, JSFE refuses to run the action without the gate, binds the confirmation to the exact request the caller
134
+ heard, invalidates it when any input changes, and never treats a timeout as success.
135
+
136
+ **An unknown result.** A timeout after the card may have been charged leaves the result unknown. The engine handles it
137
+ for every action: the action is claimed once, a result that is not one of the declared `outcomes` counts as unknown,
138
+ and an unknown charge or cancellation is held, never repeated (only a `send` may declare an explicit retry). Where the
139
+ payment API accepts an idempotency key, the flow passes one as a tool argument as well.
140
+
141
+ ### Shared data and shared functions
142
+
143
+ A value several flows use (a catalog, a list of services, a set of phrases) is written once in the set's data entry and
144
+ referenced as `{"$data": "path.to.value"}`; expressions read it as `$data.name`. A function several flows use is
145
+ written once in the set's functions entry and called as `fn.name(...)`. One source means two flows can never drift
146
+ apart.
147
+
148
+ ### Session state
149
+
150
+ The engine's session holds the flow stacks and `cargo`, the conversation's shared state. Form progress lives in
151
+ `cargo.forms`, so a task interrupted by a question, a detour or a different request resumes from where it was when the
152
+ caller comes back to it. The host stores the session between turns; the engine never stores anything itself.
153
+
154
+ ### Deploying while conversations are in progress
155
+
156
+ A new flow set can go live while callers are in the middle of a flow. A flow's `version` decides what happens to them:
157
+
158
+ - **A minor or patch change** (`1.4.0` to `1.5.0`: new wording, a new fact, a fix) keeps the conversation going on the
159
+ steps already saved in it.
160
+ - **A breaking change bumps the major** (`1.5.0` to `2.0.0`): an input, entry argument or shared name removed or
161
+ renamed, or a value whose meaning changed. A conversation saved under the old major ends that task safely on its next
162
+ turn, without running any action or reading the caller's message as an answer, and the caller is asked to state the
163
+ request again.
164
+
165
+ `continuationBreaks(previousFlows, nextFlows)` compares a set with the deployed one and lists every removed name that
166
+ needs a major bump, so a forgotten bump fails the deploy instead of reaching a caller.
167
+
168
+ ## 4. One turn, step by step
169
+
170
+ Every caller message travels the same path. The order is the design: **deterministic readers first, AI only where
171
+ language is genuinely ambiguous, and the host last.**
172
+
173
+ ```mermaid
174
+ flowchart TD
175
+ M[Caller message] --> A{Flow waiting for an answer?}
176
+ A -->|yes| R[Deterministic readers: keys, numbers, typed fields, exact phrases]
177
+ R -->|read| V[Validate, store, ask next question]
178
+ R -->|not read| K[Reply ranker: a closed choice]
179
+ K -->|answers / a choice| V
180
+ K -->|abort| X[End flows: request cancelled]
181
+ K -->|new_request| D[DISPATCH]
182
+ A -->|no| D
183
+ D --> S1[Intent detection, stage 1: which primary flow, or None]
184
+ S1 -->|a flow| S2[Stage 2: that flow's parameters]
185
+ S2 --> F[Start the flow; its form validates every value]
186
+ S1 -->|None| HA[Host answers with its own AI]
187
+ ```
188
+
189
+ ### Deterministic readers
190
+
191
+ When a flow is waiting for an answer, the engine first tries to read it without any model: a keypad key, a phone number,
192
+ an email, an amount, a card number, a whole-answer phrase of a choice. The rules are strict on purpose:
193
+
194
+ - **The current question first.** An answer is read against the question being asked, and the inputs that question
195
+ explicitly invites ("type NEW CARD, LINK, or a different amount" invites an amount).
196
+ - **Out of order needs the whole signature.** A number fills another field only when the whole number fits it: a
197
+ fragment of a longer number fills nothing.
198
+ - **A confirmation is answered only when it is asked.** A stray "yes" never confirms anything that is not the current
199
+ question.
200
+ - **One part of an answer never fills two fields.** Two fields of the same form that fit the same words are asked
201
+ about, never ranked.
202
+
203
+ The engine records every out-of-order fill and every refused reading, so a host can log why a value was taken.
204
+
205
+ ### The reply ranker
206
+
207
+ When the readers cannot read an answer that has words in it, the engine asks the **reply ranker** one question: what
208
+ does this reply do with the question the flow asked? The model picks from a closed list:
209
+
210
+ | Option | Meaning | What happens |
211
+ | --- | --- | --- |
212
+ | `answers` | the reply responds to the question | the flow continues; any value still goes through deterministic validation |
213
+ | `choice:<key>` | the reply picks one of the question's offered choices | that choice is the answer |
214
+ | `abort` | the caller withdraws the request | every flow ends; the caller hears that the request was cancelled |
215
+ | `new_request` | the caller asks for something else | DISPATCH: the message goes to intent detection |
216
+
217
+ Some options are **never offered**, so the ranker cannot pick them: a choice marked `exact` (anything that charges or
218
+ cancels) and the YES of a firm confirmation. A ranker can understand "no thanks, maybe later" as NO; it can never
219
+ produce a YES to a payment. Options that cost more when wrong need a higher score (a NO that would cancel what the
220
+ caller was confirming, or a new request that leaves the question unanswered). A reply that clears no option is
221
+ `answers`, and the question is asked again if nothing usable was in it.
222
+
223
+ ### DISPATCH
224
+
225
+ `DISPATCH` is the bridge back to free conversation. It ends the flows in progress and re-reads the message through
226
+ intent detection, in the same turn. If the message starts another flow, that flow starts; if not, the host answers it
227
+ with its own AI and the conversation context. Form progress is kept in `cargo.forms`, so when the caller returns to
228
+ the task ("OK, let's pay now"), the primary flow starts again and resumes from its saved draft instead of asking
229
+ everything again.
230
+
231
+ A flow can also ask a question whose answer is open ("Describe the problem") with a **contextual DISPATCH**
232
+ (`onResponse: "continue"`): the ranker decides whether the reply answers, withdraws or changes the subject, and an
233
+ answer's fields are extracted by the same parameter extraction intent detection uses.
234
+
235
+ **DISPATCH is not giving up.** Sending a caller to a person (the `give-up` worker) happens only after real attempts at
236
+ the answer ran out, or a service the flow needs failed. A caller who changed the subject is dispatched, never
237
+ transferred.
238
+
239
+ ### Intent detection: two stages
240
+
241
+ 1. **Which flow.** Only primary flows can start this way, and stage 1 reads only their `description`: what should
242
+ start the flow and, in a `Not:` list, what belongs elsewhere. The answer is one flow or `None`.
243
+ 2. **Which arguments.** For the chosen flow only, stage 2 extracts its parameters: closed values (an enum such as
244
+ `asked: "balance"`, a boolean) and free text. A parameter the model cannot fill is left out, and the flow asks for
245
+ it.
246
+
247
+ Each stage is one `aiCallback` call that carries instructions for both a classifier and an LLM, so a host can run
248
+ either, or race both. JSFE builds both sets of instructions and decodes both answers (`jsfe/host`), which keeps a
249
+ production host and a test harness on one implementation.
250
+
251
+ ## 5. Why the transactions stay safe
252
+
253
+ The conversational freedom above never reaches the transaction directly. These are the guarantees, and where each one
254
+ lives:
255
+
256
+ | Guarantee | How |
257
+ | --- | --- |
258
+ | No private fact before identity is verified | a provider's `gate` and a performer's `gate` run the verification worker and check the verified paths |
259
+ | No value accepted unchecked | every value, whether read, extracted at activation or understood by the ranker, passes the same field validator |
260
+ | No charge without a clear YES | `confirm: { exact: true }`: only confirmation words or the option's exact phrases; never offered to the ranker |
261
+ | The YES confirms what the caller heard | consent is bound to the request and the input revision; changing any input invalidates it |
262
+ | "Yes, but make it 60" is not consent | an answer with YES plus a correction or another request is not approval of the old request |
263
+ | No guessed success | an executor reports success, failure or unknown; unknown holds the action, it is never retried as if it failed |
264
+ | One effect, once | effects run only through `CALL-TOOL`; the session records the attempt (`lastTurnToolCalls`) and the host never rolls such a turn back |
265
+ | No escape from a half-finished effect | `must_complete` keeps a payment's card questions in place instead of dispatching an unreadable answer |
266
+
267
+ The host keeps its side of the contract: one `updateActivity` at a time per conversation; a cancelled turn is awaited,
268
+ not abandoned; a turn that attempted a tool is never restored to its earlier session; and external effects carry an
269
+ idempotency key where the backend supports one. The [README](../README.md) section "Host Responsibilities: Tool Calls,
270
+ Cancellation and Concurrency" explains each rule and the production incident behind it.
271
+
272
+ ## 6. What the caller sees and hears
273
+
274
+ ### Messages
275
+
276
+ - **`SAY` and `SAY-GET`** carry `value` and a per-language variant (`value_es`); the session's language picks one.
277
+ - **Voice and text differ.** A question may have written wording for text (`question_text`) and spoken wording for a
278
+ call. On a call, offer keys ("press 1 or say YES"); in text, never say "press".
279
+ - **Spoken values.** Helpers read amounts, dates and last-four digits naturally on a call (`amountToSpeech`,
280
+ `dateToWords`, `last4`).
281
+
282
+ ### Questions and answers
283
+
284
+ - **Choices.** A choice has a key, phrases in each language and, on a call, keypad keys. A key matches only as the
285
+ whole answer. Sensitive choices are `exact`.
286
+ - **Confirmations.** `confirmation: "firm"` for consent (a charge, a cancellation, storing a card, filing a request):
287
+ only confirmation words. `"soft"` for an offer ("Would you like a link?"): the wider YES vocabulary.
288
+ - **Proposals.** When the flow infers a value ("Is this the card ending in 4242?"), a proposal asks the caller to
289
+ approve that one value. It never authorizes the final action.
290
+ - **Questions in the middle of a task.** "When is it due?" asked while paying is answered from the provider's catalog,
291
+ and the payment asks its question again.
292
+ - **Menus after an answer.** A provider may offer a short menu ("To make a payment, say PAY. For a payment link, say
293
+ LINK."). Menu choices start their flows; "no thanks" ends politely.
294
+ - **Misses.** The first unreadable answer is acknowledged and the question is asked again; validator rejections have
295
+ their own budget (`max_attempts`) with a declared recovery (`on_exhausted`). Every question also accepts a request
296
+ for a person.
297
+
298
+ ### Endings
299
+
300
+ A flow ends with an outcome (`resolved`, `unresolved`, `deflected`, or `pending` for a task that was paused), and may
301
+ give the host recovery guidance (`resume`, `restart`, `hold`, `none`). Hosts use these for reporting and for what to
302
+ say when the conversation returns.
303
+
304
+ ## 7. A conversation, annotated
305
+
306
+ ```text
307
+ Caller: Hi, I want to pay my bill.
308
+ ```
309
+ Intent detection, stage 1, picks `PayBill`; stage 2 finds no amount. The form's first missing input is the account.
310
+
311
+ ```text
312
+ Agent: To make a payment, I first need to verify your account. Please say or enter your account number.
313
+ Caller: 4417 2209
314
+ Agent: I found the account. Please enter the 6-digit code I just sent to your phone ending in 12.
315
+ Caller: 381 552
316
+ ```
317
+ Deterministic readers: the account number and the code are typed fields. The identity gate is now satisfied.
318
+
319
+ ```text
320
+ Agent: Your amount due is $84.20. How much would you like to pay?
321
+ Caller: When is it due, actually?
322
+ Agent: Your payment is due on November 5. How much would you like to pay?
323
+ ```
324
+ The question is a provider fact asked mid-form: answered, and the form asks its question again.
325
+
326
+ ```text
327
+ Caller: Let's do 60 with the card ending in 4242.
328
+ ```
329
+ One answer fills two fields: the amount (the current question) and the card (an input of the nested card form).
330
+
331
+ ```text
332
+ Agent: I'll charge $60.00 to the card ending in 4242. Say YES to pay, or NO.
333
+ Caller: Yeah, but make it 70.
334
+ ```
335
+ Not consent: YES plus a correction. The amount changes, the confirmation is invalidated, and the read-back is asked
336
+ again for $70.00.
337
+
338
+ ```text
339
+ Agent: I'll charge $70.00 to the card ending in 4242. Say YES to pay, or NO.
340
+ Caller: Yes.
341
+ Agent: Your payment of $70.00 was successful. Is there anything else I can help you with?
342
+ Caller: Do you have energy-saving rebates?
343
+ ```
344
+ An exact YES to the current confirmation runs the charge once. The last question is not something this flow reads:
345
+ the ranker says `new_request`, DISPATCH sends it to intent detection, no flow serves it, and the host's own AI answers
346
+ it from the company's documents.
347
+
348
+ ## 8. Design rules worth keeping
349
+
350
+ 1. **Declare, don't script.** Write the manifest (inputs, facts, actions) first; let JSFE generate the steps.
351
+ 2. **Support what the question invites, DISPATCH the rest.** Never build a natural-language interpreter for one field.
352
+ 3. **One source for everything.** Shared workers, shared data, shared functions: copies drift.
353
+ 4. **Flow behavior lives in flows.** Offers, prompts and wording belong to flow steps, not to the host. If a flow cannot
354
+ express something, it is a feature request for the engine.
355
+ 5. **Side effects only through `CALL-TOOL`,** with an `onFail` route and a declared answer for an unknown result.
356
+ 6. **Sensitive decisions are exact.** Any choice that charges or cancels is `exact`, and asked as a firm confirmation.
357
+ 7. **Descriptions are for detection only.** A primary's description says what starts it and what does not; caller
358
+ wording belongs in questions and fact renderers.
359
+ 8. **Test journeys, not just units.** Run the real engine on the real set: in order, out of order, all at once,
360
+ corrected, a mid-form question, a change of subject, a decline, voice and text, every language.
361
+
362
+ ## Where to read next
363
+
364
+ - [Flow author reference](copilot-reference.md): every field, step, profile and rule, precisely.
365
+ - [README](../README.md): engine API, `aiCallback`, intent detection, the reply ranker and host responsibilities.
366
+ - [System flows](flows/system/README.md): the shared workers, a page and a chart per flow.
@@ -328,7 +328,6 @@ A Performer requires `form`, nonempty `performs`, `prepare` worker and `present`
328
328
  | `subject` | Ownership path such as `form.data.account` or `form.data.subject`; bind account/service identity, not an unrelated session flag. |
329
329
  | `gate:{flow?,verified:[paths]}` | Execution-time verification; rerun the declared flow before each fresh effect. |
330
330
  | `flow`, `tool` | Installed executor worker and/or registered tool identity. |
331
- | `idempotency`, `reconcile` | Explicit backend policy. Charge/write/send/cancel require at least one; declaring prose alone does not implement backend idempotency. |
332
331
  | `confirm:{exact:true}` | Required for charge, cancel or `reversible:false`. This requires a real form confirmation question. |
333
332
  | `outcomes` | Map declared executor statuses to business outcomes, including unknown results. |
334
333
  | `retry` | Explicit policies: `after:[knownStatuses]`; unknown retry only for SEND with `unknown:"explicit"` and a new accepted recovery choice. |
@@ -359,7 +358,6 @@ This reduced **composed-registry fragment** illustrates action authoring, not a
359
358
  "action":"curacao_payment", "effect":"charge", "subject":"form.data.account",
360
359
  "gate":{"flow":"get-account","verified":["cargo.otpVerified","cargo.authenticatedAccount"]},
361
360
  "flow":"payment-execute", "confirm":{"exact":true},
362
- "reconcile":{"policy":"Hold unknown results for reconciliation; never automatically repeat a charge."},
363
361
  "outcomes":{"ok":"resolved","failed":"unresolved","unknown":"unresolved"}
364
362
  }],
365
363
  "prepare":"payment-prepare", "present":"payment-present",
@@ -378,7 +376,7 @@ This reduced **composed-registry fragment** illustrates action authoring, not a
378
376
  }
379
377
  ```
380
378
 
381
- The action key makes this session projection idempotent; it does not make the payment API idempotent. Declare backend `idempotency` only when the actual API supports the stated key/policy. Otherwise implement and test the declared reconciliation/unknown-result hold. Neither policy wording nor a recorded ledger entry proves a charge succeeded.
379
+ The action key makes this session projection idempotent; it does not make the payment API idempotent. Where the API accepts an idempotency key, pass one as a tool argument. Either way an unknown result is held, never repeated: the engine claims each confirmed action once, treats a status outside `outcomes` as unknown, and never retries an unknown charge or cancellation. A recorded ledger entry does not prove a charge succeeded.
382
380
 
383
381
  ## 9. Outcomes, recovery and persistence
384
382
 
@@ -394,7 +392,7 @@ Session-only protects persisted, serialized session progress from reentry/repeti
394
392
 
395
393
  With a durable journal, the host saves the returned full session before acknowledging exact versioned action receipts with `checkpointActionSession`. Failed saves do not release scopes. Compatible late results project into current state without replaying presenters or restoring an old workflow merely because their storage sequence is larger. Recovery identity binds verified runtime bytes and actual flow/tool contracts; a version string alone is insufficient. Identity mismatch, unresolved effects or failed projections preserve holds. Do not change persistence mode for a live conversation or clear holds to manufacture compatibility.
396
394
 
397
- For versioned JSON, JSFE generates internal continuation revisions from the entire ordered declaration and named function bodies. Do not author `safetyRevision`. A source change retires obsolete unexecuted continuations before new input; uncertain effects remain preserved/quarantined. This is change detection, not authentication or automatic hot reload. Ordinary executable flows can explicitly use the SDK's revision helper when their host owns that policy.
395
+ For versioned JSON, JSFE generates each declaration's continuation revision from its `version` major, plus the `version` major of each shared data or functions entry it uses (`$data`, `fn.`). Do not author `safetyRevision`. A minor or patch change keeps the revision: a saved session continues on its saved steps. **A change that can break a saved session bumps the major** (a removed or renamed input, entry argument, shared data or function name, or a changed meaning under the same name); the next turn of a session saved under the old major retires its unexecuted continuation before new input, and uncertain effects remain preserved/quarantined. `continuationBreaks(previousFlows, nextFlows)` (`jsfe/forms`) lists the removed names that need a major bump; run it against the deployed set before every deploy. A session saved under the earlier fingerprint revision (the whole declaration) continues while its declaration is unchanged. This is change detection, not authentication or automatic hot reload. Ordinary executable flows can explicitly use the SDK's revision helper when their host owns that policy.
398
396
 
399
397
  ## 10. Matching tools JSON
400
398
 
@@ -87,7 +87,13 @@ group being read as another field (an account number, an amount).
87
87
 
88
88
  ## 1.9.2: address city list and part corrections
89
89
 
90
- The shared address form (`collect-geocoded-address`) asks the city when the ZIP code carries several place names (`geocode-zip` returns `localities`, Google's `postcode_localities`; at most 7): "Which city is it: Commerce, East Los Angeles, Los Angeles or Vernon?", by name or key. With one name it is not asked, and a caller who says the city is wrong is asked it plainly and any city is taken. At the read-back, naming one part without a value ("the city is wrong", "la ciudad no") asks only that part again, and a plain NO asks which part is wrong (zip code, city, street, apartment, or ALL to start over) instead of starting over. The part naming is an opt-in form confirmation setting, `confirm.named_fields: true`, read from each input's `names`; other forms keep their behavior. A host whose `geocode-zip` returns no `localities` keeps the one city it returns.
90
+ The shared address form (`collect-geocoded-address`) asks the city when the ZIP code carries several place names (`geocode-zip` returns `localities`, Google's `postcode_localities`; at most 7): "Which city is it: Commerce, East Los Angeles, Los Angeles or Vernon?", by name or key. With one name it is not asked, and a caller who says the city is wrong is asked it plainly and any city is taken. At the read-back, naming one part without a value ("the city is wrong", "la ciudad no") asks only that part again, and a plain NO asks which part is wrong (zip code, city, street, apartment, or ALL to start over) instead of starting over. The part naming is an opt-in form confirmation setting, `confirm.named_fields: true`, read from each input's `names`; other forms keep their behavior. A host whose `geocode-zip` returns no `localities` keeps the one city it returns. A city read from the ZIP code alone is replaced by the city of an exact (rooftop) street match (90805: the ZIP lookup names Lakewood, its streets are Long Beach and Lakewood); a city the caller chose or gave is never replaced. With `named_fields`, a correction the word reader cannot read ("es Long Beach") goes to the reply ranker, which is offered each named part read back (`field:<input>`); a pick asks that part again, so the caller's value replaces the one read back.
91
+
92
+ At the verification code question the reply ranker is offered `where`: a caller who asks where the code is ("¿dónde está?") hears where it went (by text to the cell ending with the last four digits, by email, or in this chat) and the code question again; nothing is sent again.
93
+
94
+ The contact question's `intro` (why the account is needed) is not lost when the question is answered without being asked (the caller ID on file, a contact the caller gave): the first verification code question begins with it instead, once. A contact question that is asked says it, as before, and the code question does not repeat it.
95
+
96
+ A form's or performer's `provides` catalog is validated like a provider's (`validateFlowContracts`: facts present, unique keys, a source path or worker for each fact, a gate for private facts, known gate and source workers), and every catalog's menu choices must name existing flows (a FLOW to an unknown name is skipped silently at run time). The check runs where flow sets are built and validated, never when an engine loads a set.
91
97
 
92
98
  ## 1.9.1: stored sessions without definition copies
93
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.15.9",
3
+ "version": "1.17.0",
4
4
  "description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
5
5
  "license": "MIT",
6
6
  "author": "Ron Pinkas",
@@ -43,6 +43,7 @@
43
43
  "dist",
44
44
  "README.md",
45
45
  "LICENSE",
46
+ "docs/architecture-guide.md",
46
47
  "docs/copilot-reference.md",
47
48
  "docs/development-release.md"
48
49
  ],