Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ conformance/
<case>.yaml
<case>.expected.json
v2/ # `book` — same layout, its own frozen cases
vN/outbound/ # optional: the outbound profile
valid/ invalid/
```

`vN/outbound/` holds cases for the **outbound** gate (`validate_outbound` /
`validateOutbound`): a flow run on a call the app places, with no person
standing by. Same file pair and expectation shape; `semantic` is the verdict
of the outbound gate, which includes the base validation. These cases stay
out of the top-level `valid/`/`invalid/` buckets because a flow refused for
outbound — one with a `ring` step, say — is a perfectly good inbound flow.

Every `<case>.yaml` has a sibling `<case>.expected.json`:

```json
Expand Down
5 changes: 5 additions & 0 deletions conformance/v1/outbound/valid/reminder.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "A typical outbound flow: say something, ask the callee to confirm, and end. It loops (the menu's `2` replays the greeting), which is fine: the loop runs through a menu, which waits for the callee, and the menu's `no_input` exit ends the call when nobody answers.",
"structurallyValid": true,
"semantic": { "ok": true, "errors": [] }
}
20 changes: 20 additions & 0 deletions conformance/v1/outbound/valid/reminder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
schema_version: 1
id: flow_reminder
name: Appointment reminder
entry: hello
nodes:
hello:
kind: greeting
prompt: Hello, this is a reminder about your appointment tomorrow at ten.
exits: { next: ask }
ask:
kind: menu
prompt: Press 1 to confirm, or 2 to hear this again.
options: { "1": Confirm, "2": Repeat }
exits: { "1": thanks, "2": hello, no_input: bye, invalid: ask }
thanks:
kind: hangup
prompt: Thanks, see you then.
bye:
kind: hangup
prompt: Sorry we missed you. Goodbye.
5 changes: 5 additions & 0 deletions conformance/v2/outbound/invalid/book.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "A `book` step books against the number that called in, which a call the app places does not have, so outbound refuses it. The same document is valid as an inbound flow.",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["outbound_node_not_allowed"] }
}
16 changes: 16 additions & 0 deletions conformance/v2/outbound/invalid/book.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
schema_version: 2
id: flow_outbound_book
name: Outbound with a booking step
entry: take_booking
nodes:
take_booking:
kind: book
prompt: I can book you in.
confirm_prompt: You're booked for
timezone: UTC
schedule:
tue: [{ open: "09:00", close: "17:00" }]
duration_mins: 30
exits: { booked: bye, no_slots: bye, no_input: bye, unavailable: bye }
bye:
kind: hangup
5 changes: 5 additions & 0 deletions conformance/v2/outbound/invalid/dangling-exit.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "The outbound gate includes the base validation: a document the base validator rejects is rejected on outbound too, with the same codes.",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["unknown_target"] }
}
11 changes: 11 additions & 0 deletions conformance/v2/outbound/invalid/dangling-exit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
schema_version: 2
id: flow_outbound_dangling
name: Outbound with a broken exit
entry: hello
nodes:
hello:
kind: greeting
prompt: Hello.
exits: { next: nowhere }
bye:
kind: hangup
5 changes: 5 additions & 0 deletions conformance/v2/outbound/invalid/greeting-loop.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Two greetings that hand off to each other: the loop never waits for the callee. Reported as an unbounded loop, alongside the base validator's `trapped` (nothing in it can end the call).",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["outbound_unbounded_loop", "trapped"] }
}
13 changes: 13 additions & 0 deletions conformance/v2/outbound/invalid/greeting-loop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
schema_version: 2
id: flow_outbound_greeting_loop
name: Says the same thing forever
entry: one
nodes:
one:
kind: greeting
prompt: Hello.
exits: { next: two }
two:
kind: greeting
prompt: Hello again.
exits: { next: one }
5 changes: 5 additions & 0 deletions conformance/v2/outbound/invalid/hours-loop.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "During opening hours, hours -> greeting -> hours repeats with nothing waiting on the callee; played into voicemail it would never end. The `closed` exit keeps it from being trapped, so the base validator accepts it — this is the case only the outbound rule catches.",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["outbound_unbounded_loop"] }
}
17 changes: 17 additions & 0 deletions conformance/v2/outbound/invalid/hours-loop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
schema_version: 2
id: flow_outbound_hours_loop
name: Repeats while open
entry: check
nodes:
check:
kind: hours
timezone: UTC
schedule:
mon: [{ open: "09:00", close: "17:00" }]
exits: { open: say, closed: bye }
say:
kind: greeting
prompt: We are open.
exits: { next: check }
bye:
kind: hangup
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "The menu's no_input exit leads back to the greeting, so with nobody pressing keys (voicemail) hello -> ask -> hello never ends. Pressing 1 or an unmapped key still ends the call, so the base validator accepts it — a loop through a menu is only safe on its digit exits.",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["outbound_unbounded_loop"] }
}
19 changes: 19 additions & 0 deletions conformance/v2/outbound/invalid/menu-no-input-loop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
schema_version: 2
id: flow_outbound_menu_no_input_loop
name: Repeats into voicemail
entry: hello
nodes:
hello:
kind: greeting
prompt: Hello, this is a reminder about your appointment tomorrow at ten.
exits: { next: ask }
ask:
kind: menu
prompt: Press 1 to confirm.
options: { "1": Confirm }
exits: { "1": thanks, no_input: hello, invalid: bye }
thanks:
kind: hangup
prompt: Thanks, see you then.
bye:
kind: hangup
5 changes: 5 additions & 0 deletions conformance/v2/outbound/invalid/ring.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "A `ring` step hands the call to a person at the device. On a call the app places there is nobody standing by to take it, so outbound refuses the step. The same document is valid as an inbound flow.",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["outbound_node_not_allowed"] }
}
15 changes: 15 additions & 0 deletions conformance/v2/outbound/invalid/ring.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
schema_version: 2
id: flow_outbound_ring
name: Outbound with a ring step
entry: hello
nodes:
hello:
kind: greeting
prompt: Please hold while we connect you.
exits: { next: desk }
desk:
kind: ring
timeout_secs: 20
exits: { no_answer: bye }
bye:
kind: hangup
5 changes: 5 additions & 0 deletions conformance/v2/outbound/invalid/transfer.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "A `transfer` step is not offered on outbound calls yet, so outbound refuses it. The same document is valid as an inbound flow.",
"structurallyValid": true,
"semantic": { "ok": false, "errors": ["outbound_node_not_allowed"] }
}
12 changes: 12 additions & 0 deletions conformance/v2/outbound/invalid/transfer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
schema_version: 2
id: flow_outbound_transfer
name: Outbound with a transfer step
entry: hello
nodes:
hello:
kind: greeting
prompt: Connecting you now.
exits: { next: forward }
forward:
kind: transfer
target: "+15550100"
5 changes: 5 additions & 0 deletions conformance/v2/outbound/valid/after-hours.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Every outbound-allowed kind in one document — hours, greeting, menu, message and hangup — declared at schema_version 2. The hours step is not on a loop; the only loop goes through the menu.",
"structurallyValid": true,
"semantic": { "ok": true, "errors": [] }
}
29 changes: 29 additions & 0 deletions conformance/v2/outbound/valid/after-hours.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
schema_version: 2
id: flow_outbound_hours
name: Notice with an after-hours variant
entry: check
nodes:
check:
kind: hours
timezone: America/New_York
schedule:
mon: [{ open: "09:00", close: "17:00" }]
tue: [{ open: "09:00", close: "17:00" }]
exits: { open: notice, closed: after_hours }
notice:
kind: greeting
prompt: Your order is ready for pickup.
exits: { next: options }
options:
kind: menu
prompt: Press 1 to leave a message for the store, or 9 to hear this again.
options: { "1": Message, "9": Repeat }
exits: { "1": leave_message, "9": notice, no_input: bye, invalid: bye }
leave_message:
kind: message
prompt: Leave your message after the tone.
after_hours:
kind: hangup
prompt: Your order is ready. The store opens at nine tomorrow.
bye:
kind: hangup
11 changes: 11 additions & 0 deletions crates/wavekat-flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ if let Err(errors) = validate::validate(&flow) {
}
```

### Outbound calls

A flow run on a call the app places — with no person standing by — goes
through `validate::validate_outbound` instead. It runs `validate` first and
adds the outbound rules, so call it on its own: only `greeting`, `menu`,
`message`, `hangup` and `hours` steps are allowed
(`outbound_node_not_allowed`), and no loop may be made only of moves that
happen without the callee — a greeting that leads back to itself, or a menu
whose `no_input` exit leads back into the loop (`outbound_unbounded_loop`). A
loop through a menu's digit exits is fine.

## Upgrading when the booking grid moves

`BOOK_GRANULARITY_MINS` decides which times a `book` node can ever say, so
Expand Down
Loading
Loading