Skip to content

feat: add outbound flow validation - #48

Merged
wavekat-eason merged 3 commits into
mainfrom
feat/outbound-validation
Oct 17, 2026
Merged

wavekat-eason merged 3 commits into
mainfrom
feat/outbound-validation

Conversation

@wavekat-eason

@wavekat-eason wavekat-eason commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds an outbound profile validator for flows run on a call the app places (an automated outbound call: a flow does the talking, with no person standing by). Part of the outbound call-flow work; the voice daemon will consume the Rust half.

  • Rust: wavekat_flow::validate::validate_outbound(&Flow) -> Result<(), Vec<ValidationError>>, plus validate::OUTBOUND_KINDS.
  • TS: validateOutbound(flow, nodeRanges?) -> Issue[] and OUTBOUND_KINDS, exported from @wavekat/flow-schema.
  • Includes the base validation: each runs validate / validateFlow first and adds the outbound issues, so it is the one gate to call for an outbound flow (no need to call both).

Rules

Code Rule
outbound_node_not_allowed Only greeting, menu, message, hangup, hours are allowed. ring (nobody to hand the call to), book (books against the inbound caller's number) and transfer (not offered outbound yet) are refused. One issue per node, naming the node id and kind (params.kind in TS).
outbound_unbounded_loop No cycle reachable from entry made only of moves that happen without the callee: any exit of a greeting or hours step, or a menu's no_input exit (taken every time on voicemail). Reported once per node on such a cycle. A loop through a menu's digit or invalid exits is fine: only a callee pressing keys can take it.

The loop rule overlaps trapped for pure greeting cycles (both are reported), but also catches what the base validator accepts: hours -> greeting -> hours with closed escaping, which repeats forever during opening hours.

In Rust these are two new ValidationError variants (OutboundNodeNotAllowed { node, kind }, OutboundUnboundedLoop { node }); no new error type.

Conformance

New conformance/vN/outbound/{valid,invalid} buckets (documented in conformance/README.md), run by both runners through the outbound gate with the usual subset contract, plus the structural check. They stay out of the top-level buckets because a flow refused outbound is still a valid inbound flow.

  • v1/outbound/valid/reminder: greeting + menu loop
  • v2/outbound/valid/after-hours: every allowed kind
  • v2/outbound/invalid/{ring,book,transfer}: outbound_node_not_allowed
  • v2/outbound/invalid/hours-loop: outbound_unbounded_loop (base-valid)
  • v2/outbound/invalid/menu-no-input-loop: outbound_unbounded_loop (base-valid; menu no_input leads back to the greeting)
  • v2/outbound/invalid/greeting-loop: outbound_unbounded_loop + trapped
  • v2/outbound/invalid/dangling-exit: unknown_target (proves the base validation runs)

Notes for the voice daemon (currently on 0.0.6)

Between crate 0.0.6 and 0.0.9 (main):

  • 0.0.8, breaking: book::BOOK_GRANULARITY_MINS moved 15 -> 30, so vocabulary_refs / required_assets return a smaller set for the same book node. Per the crate README, take the crate upgrade before the authoring side takes the matching @wavekat/flow-schema.
  • 0.0.7 added validate::required_schema_version; 0.0.9 added book::vocabulary_refs_on. Additive.
  • This PR adds two variants to ValidationError, which is not #[non_exhaustive]: an exhaustive match on it outside the crate would need new arms (same as when KindRequiresNewerSchema arrived). Matching on .code() is unaffected.

Versioning is left to release-please (feat: -> next patch on both packages).

Test plan

  • cargo fmt --check: clean
  • cargo clippy --all-targets -- -D warnings: clean
  • cargo test -p wavekat-flow: 64 unit tests passed (8 new outbound), 5 conformance tests passed (new outbound_corpus_matches_expectations). Checked that flipping an outbound expectation makes it fail.
  • cargo package -p wavekat-flow: builds from its tarball
  • pnpm --filter @wavekat/flow-schema gen: no drift in generated files
  • pnpm --filter @wavekat/flow-schema typecheck: clean
  • pnpm --filter @wavekat/flow-schema test: 10 files, 161 tests passed (new outbound.test.ts, 8 tests; 11 new outbound conformance tests)
  • pnpm --filter @wavekat/flow-schema build: ok

🤖 Generated with Claude Code

https://claude.ai/code/session_01QGHE3tcTFWUtjDUxNxTWNG

wavekat-eason and others added 3 commits October 17, 2026 17:45
Add an outbound profile for flows run on a call the app places, with no
person standing by: `validate::validate_outbound` (Rust) and
`validateOutbound` (TS). Each runs the base validation first and adds:

- outbound_node_not_allowed: only greeting, menu, message, hangup and
  hours steps are allowed (no ring, book or transfer).
- outbound_unbounded_loop: no loop reachable from entry made only of
  steps that move on without the callee (greeting, hours). Loops
  through a menu are fine.

Shared cases live in conformance/vN/outbound/{valid,invalid}, run by
both languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGHE3tcTFWUtjDUxNxTWNG
Rust 1.99's new clippy::double_must_use lint fires on code generated
by async-trait 0.1.91, failing `clippy -D warnings`. 0.1.92 fixes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNFHhzw4ZsqCFnv6ERigjd
A loop through a menu was always accepted, but with nobody pressing keys
(voicemail, a silent callee) the menu takes no_input every time. If that
exit leads back into the loop, the call never ends. Count a menu's
no_input exit as a silent move, alongside greeting and hours exits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNFHhzw4ZsqCFnv6ERigjd
@wavekat-eason
wavekat-eason marked this pull request as ready for review October 17, 2026 06:54
@wavekat-eason
wavekat-eason merged commit 0dad982 into main Oct 17, 2026
3 checks passed
@wavekat-eason
wavekat-eason deleted the feat/outbound-validation branch October 17, 2026 06:54
@github-actions github-actions Bot mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant