booking-agent-cli 0.11.2__tar.gz → 0.12.0__tar.gz

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 (377) hide show
  1. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/PKG-INFO +2 -2
  2. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/README.md +1 -1
  3. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/pyproject.toml +1 -1
  4. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/__init__.py +1 -1
  5. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/backend/__init__.py +34 -2
  6. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/backend/live.py +38 -1
  7. booking_agent_cli-0.12.0/src/booking_agent/booking_questions.py +482 -0
  8. booking_agent_cli-0.12.0/src/booking_agent/booking_questions_negotiation.py +103 -0
  9. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/discovery.py +2 -1
  10. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/headless.py +3 -2
  11. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/http_trace.py +2 -2
  12. booking_agent_cli-0.12.0/src/booking_agent/models/booking_questions.py +294 -0
  13. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/models/platform.py +13 -0
  14. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/models/wire.py +105 -4
  15. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/namespace.py +2 -0
  16. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/rest_client.py +75 -1
  17. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/ucp_checkout.py +31 -6
  18. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/ucp_negotiation.py +15 -8
  19. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/usp_methods.py +2 -0
  20. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/__init__.py +1 -1
  21. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/activity_log.py +12 -2
  22. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/booking_poll.py +5 -1
  23. booking_agent_cli-0.12.0/src/booking_agent_cli/booking_questions_flow.py +854 -0
  24. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/booking_summary.py +57 -2
  25. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/bundled_skill.md +56 -52
  26. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/calendar_event.py +10 -1
  27. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/commands/flow.py +1692 -304
  28. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/engine.py +8 -0
  29. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/payment_helpers.py +446 -9
  30. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/session.py +80 -0
  31. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/support_report.py +12 -0
  32. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/conftest.py +10 -1
  33. booking_agent_cli-0.12.0/tests/e2e/test_prepared_checkout_e2e.py +130 -0
  34. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/test_ucp_skill_flow_e2e.py +47 -27
  35. booking_agent_cli-0.12.0/tests/evals/booking_questions_relay.json +77 -0
  36. booking_agent_cli-0.12.0/tests/integration/test_booking_questions_flow.py +408 -0
  37. booking_agent_cli-0.12.0/tests/integration/test_booking_questions_relay.py +74 -0
  38. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_cli_enforced_gates.py +18 -3
  39. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_flow_gates.py +10 -5
  40. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_home_visit_address.py +3 -2
  41. booking_agent_cli-0.12.0/tests/integration/test_prepared_checkout.py +216 -0
  42. booking_agent_cli-0.12.0/tests/integration/test_recipient_flow.py +138 -0
  43. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_session_compat.py +25 -0
  44. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_support_report.py +7 -1
  45. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_ucp_checkout_validate.py +31 -0
  46. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/SOURCE +1 -1
  47. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/availability.json +3 -2
  48. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/booking.json +69 -27
  49. booking_agent_cli-0.12.0/tests/spec_schemas/booking_questions.json +1640 -0
  50. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/catalog.json +159 -15
  51. booking_agent_cli-0.12.0/tests/spec_schemas/esp.json +365 -0
  52. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/paid_bookings.json +40 -3
  53. booking_agent_cli-0.12.0/tests/spec_schemas/pay_at_service.json +167 -0
  54. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/registry.json +23 -11
  55. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/rest_common.json +2 -2
  56. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/usp.json +4 -4
  57. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/backend.py +55 -1
  58. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/ucp_checkout_store.py +37 -2
  59. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/ucp_server.py +16 -1
  60. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/offline_stack/stack.py +24 -4
  61. booking_agent_cli-0.12.0/tests/unit/test_booking_questions_negotiation.py +77 -0
  62. booking_agent_cli-0.12.0/tests/unit/test_booking_questions_privacy.py +125 -0
  63. booking_agent_cli-0.12.0/tests/unit/test_booking_questions_vectors.py +104 -0
  64. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_scoped_method_authorization.py +1 -0
  65. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_ucp_negotiation.py +76 -22
  66. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_wire_model_conformance.py +40 -1
  67. booking_agent_cli-0.12.0/tests/vectors/booking_questions/SOURCE +5 -0
  68. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-age-threshold.json +107 -0
  69. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-allergy-tree.json +167 -0
  70. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-combinators.json +270 -0
  71. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-contact-preference.json +113 -0
  72. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-depth.json +147 -0
  73. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-forward-reference.json +233 -0
  74. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-inapplicable-chain.json +135 -0
  75. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-operators.json +363 -0
  76. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-other-free-text.json +107 -0
  77. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-pruning.json +86 -0
  78. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-required-toggle.json +106 -0
  79. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-type-mismatch.json +167 -0
  80. booking_agent_cli-0.12.0/tests/vectors/booking_questions/eval/bq-cond-unanswered-reference.json +133 -0
  81. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-buyer.json +18 -0
  82. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-hold-active.json +14 -0
  83. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-q-ack-action.json +31 -0
  84. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-q-text-required.json +29 -0
  85. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-service-required-inputs.json +73 -0
  86. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-step-create.json +30 -0
  87. booking_agent_cli-0.12.0/tests/vectors/booking_questions/fixtures/bq-fx-step-submit-action.json +47 -0
  88. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-action-platform-processing.json +166 -0
  89. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-action-unsupported-fallback.json +148 -0
  90. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-answer-missing.json +135 -0
  91. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-before-confirmation-external-task.json +94 -0
  92. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-before-confirmation-recap.json +140 -0
  93. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-before-service-optional.json +97 -0
  94. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-before-service-required.json +136 -0
  95. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-configuration-duration.json +133 -0
  96. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-configuration-variable-price.json +87 -0
  97. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-consultancy-lead-source-marketing.json +114 -0
  98. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-eligible-token.json +103 -0
  99. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-exclusive-option-rejected.json +120 -0
  100. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-external-task-never-an-answer.json +127 -0
  101. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-grooming-pet-intake.json +131 -0
  102. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-group-participants-deferred.json +38 -0
  103. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-hair-salon-allergy-branch.json +166 -0
  104. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-home-cleaning-configuration.json +143 -0
  105. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ineligible-no-hold.json +110 -0
  106. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-intake-validity.json +194 -0
  107. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-kids-class-recipient-age-gate.json +138 -0
  108. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-maps-to-notes-no-duplicate.json +101 -0
  109. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-marketing-not-preticked.json +102 -0
  110. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-massage-sensitive-injury.json +124 -0
  111. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-medspa-intake-review.json +144 -0
  112. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-multiple-failures.json +132 -0
  113. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-negotiated-no-answers.json +126 -0
  114. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-no-side-effects.json +89 -0
  115. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-once-per-buyer-satisfied.json +80 -0
  116. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-pattern-description-echoed.json +108 -0
  117. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-phone-only-requirement.json +109 -0
  118. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-phone-text-ack-combined.json +261 -0
  119. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-prompt-injection-quoted.json +96 -0
  120. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-recap-redacts-sensitive.json +129 -0
  121. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-recipient-eligibility.json +116 -0
  122. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-repair-secondary-address.json +120 -0
  123. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-required-buyer-field-missing.json +73 -0
  124. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-requires-input-loop.json +111 -0
  125. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-retry-preserves-hold.json +150 -0
  126. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-revision-invalidates-intake.json +106 -0
  127. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-sensitive-never-echoed.json +124 -0
  128. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-sensitive-routes-to-url-mode.json +65 -0
  129. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-service-area.json +112 -0
  130. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-service-picker-field-withheld.json +41 -0
  131. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-set-level-path.json +118 -0
  132. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-stale-revision.json +180 -0
  133. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-tattoo-consent-policy-link.json +158 -0
  134. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-token-expired-missing.json +132 -0
  135. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-token-wrong-scope.json +123 -0
  136. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-action-authoritative.json +186 -0
  137. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-ap2-mandate-exception.json +81 -0
  138. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-create-carries-answers.json +101 -0
  139. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-external-task-escalation.json +67 -0
  140. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-incomplete-not-escalation.json +111 -0
  141. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-multi-parent-pruning.json +50 -0
  142. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-multi-service-deferred.json +99 -0
  143. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-order-echo-persists.json +156 -0
  144. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-trusted-ui-finalization.json +107 -0
  145. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-ucp-update-resend-required.json +140 -0
  146. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-under-review-then-rejected.json +143 -0
  147. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-unknown-question.json +130 -0
  148. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-unnegotiated-client.json +110 -0
  149. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-unreadable-form-withheld.json +37 -0
  150. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-update-not-allowed.json +147 -0
  151. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-update-states.json +198 -0
  152. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-uri-scheme-policy.json +122 -0
  153. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-waived-state.json +70 -0
  154. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-withdrawal-waiver-acknowledgement.json +137 -0
  155. booking_agent_cli-0.12.0/tests/vectors/booking_questions/flow/bq-flow-yoga-health-intake-validity.json +133 -0
  156. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-acknowledgement.json +79 -0
  157. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-address-flattened.json +134 -0
  158. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-boolean-not-ack.json +54 -0
  159. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-completion-requirement.json +112 -0
  160. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-compound-measurement.json +119 -0
  161. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-configuration-purpose.json +112 -0
  162. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-date-not-after-today.json +136 -0
  163. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-date-time.json +55 -0
  164. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-date-within-window.json +103 -0
  165. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-date.json +58 -0
  166. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-dropdown.json +61 -0
  167. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-email.json +81 -0
  168. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-exclusive-none.json +81 -0
  169. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-external-file.json +57 -0
  170. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-external-markup.json +58 -0
  171. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-external-signature.json +57 -0
  172. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-forbidden-keywords.json +161 -0
  173. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-ids.json +119 -0
  174. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-image-choice.json +85 -0
  175. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-limits.json +1601 -0
  176. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-localized-nested.json +262 -0
  177. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-long-text.json +69 -0
  178. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-maps-to-notes.json +97 -0
  179. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-multi-choice.json +150 -0
  180. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-notice-markdown.json +72 -0
  181. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-number.json +123 -0
  182. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-other-free-text.json +160 -0
  183. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-phone.json +87 -0
  184. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-rating.json +92 -0
  185. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-required-buyer-fields.json +168 -0
  186. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-rich-label-checkbox.json +72 -0
  187. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-section-pages.json +200 -0
  188. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-short-text.json +105 -0
  189. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-single-choice.json +335 -0
  190. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-tags.json +120 -0
  191. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-time.json +62 -0
  192. booking_agent_cli-0.12.0/tests/vectors/booking_questions/wire/bq-schema-uri.json +56 -0
  193. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/.gitignore +0 -0
  194. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/authorization.py +0 -0
  195. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/calendar/__init__.py +0 -0
  196. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/calendar/filter.py +0 -0
  197. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/calendar/google.py +0 -0
  198. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
  199. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/calendar/scope.py +0 -0
  200. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/circuit_breaker.py +0 -0
  201. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/config.py +0 -0
  202. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/defaults.env +0 -0
  203. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/faults.py +0 -0
  204. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/http_deadlines.py +0 -0
  205. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/http_retry.py +0 -0
  206. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/merchant_pacing.py +0 -0
  207. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/models/__init__.py +0 -0
  208. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/observability.py +0 -0
  209. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/platform_profile.py +0 -0
  210. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/pop.py +0 -0
  211. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/profile_cache.py +0 -0
  212. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/registry_url.py +0 -0
  213. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/secure_file.py +0 -0
  214. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/settings.py +0 -0
  215. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/signals.py +0 -0
  216. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/step_timing.py +0 -0
  217. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/ucp_capabilities.py +0 -0
  218. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent/ucp_profile.py +0 -0
  219. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/calendar_connection.py +0 -0
  220. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/calendar_delegated.py +0 -0
  221. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/calendar_preference.py +0 -0
  222. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/catalog.py +0 -0
  223. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
  224. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/commands/__init__.py +0 -0
  225. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/commands/config.py +0 -0
  226. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
  227. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/commands/runtime.py +0 -0
  228. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/commands/update.py +0 -0
  229. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/config_loader.py +0 -0
  230. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/fault_report_schema.py +0 -0
  231. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/fault_reports.py +0 -0
  232. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/flow_timings.py +0 -0
  233. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/gates.py +0 -0
  234. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/geo_eligibility.py +0 -0
  235. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/geocode.py +0 -0
  236. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/identity.py +0 -0
  237. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/install_integrity.py +0 -0
  238. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/link_cli.py +0 -0
  239. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/main.py +0 -0
  240. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/output.py +0 -0
  241. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/payment_token.py +0 -0
  242. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/provenance.py +0 -0
  243. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/reschedule.py +0 -0
  244. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/runtime.py +0 -0
  245. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/runtime_prepare.py +0 -0
  246. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/search_area.py +0 -0
  247. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/skill_bundle.py +0 -0
  248. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/skill_integrity.py +0 -0
  249. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/support_bundle_schema.py +0 -0
  250. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/timezone_context.py +0 -0
  251. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/src/booking_agent_cli/updates.py +0 -0
  252. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/.env.tests +0 -0
  253. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/__init__.py +0 -0
  254. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
  255. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/test_latency_soak.py +0 -0
  256. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/test_staging_registry_integration.py +0 -0
  257. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/test_timezone_booking_e2e.py +0 -0
  258. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
  259. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/e2e/ucp_native.py +0 -0
  260. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/__init__.py +0 -0
  261. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_activity_log.py +0 -0
  262. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_at_rest_hygiene.py +0 -0
  263. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_booking_rejection.py +0 -0
  264. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_bulk_cancel.py +0 -0
  265. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_calendar_connection.py +0 -0
  266. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_calendar_connector.py +0 -0
  267. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_calendar_delegated.py +0 -0
  268. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_calendar_flow.py +0 -0
  269. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_calendar_preference.py +0 -0
  270. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_checkout_signals.py +0 -0
  271. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_circuit_breaker.py +0 -0
  272. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_config.py +0 -0
  273. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_correlation_flow.py +0 -0
  274. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_declined_spt.py +0 -0
  275. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_discover_registry_rejection.py +0 -0
  276. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_flow_evidence.py +0 -0
  277. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_flow_reschedule.py +0 -0
  278. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_flow_step_timings.py +0 -0
  279. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_google_calendar_oauth.py +0 -0
  280. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_http_trace.py +0 -0
  281. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_identity_logout.py +0 -0
  282. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_install_integrity.py +0 -0
  283. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_lazy_oauth.py +0 -0
  284. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_link_auth.py +0 -0
  285. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_link_cli_spt.py +0 -0
  286. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_namespace.py +0 -0
  287. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_oauth_listener.py +0 -0
  288. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_oauth_metadata.py +0 -0
  289. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_platform_key_pop.py +0 -0
  290. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_platform_profile.py +0 -0
  291. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_pop_keystore.py +0 -0
  292. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_profile_cache.py +0 -0
  293. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_regression_index.py +0 -0
  294. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
  295. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_request_retry_semantics.py +0 -0
  296. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_resume_after_restart.py +0 -0
  297. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_runtime_prepare.py +0 -0
  298. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_search_area.py +0 -0
  299. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_session_retention.py +0 -0
  300. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_skill_integrity.py +0 -0
  301. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_skill_portability.py +0 -0
  302. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_timezone_context.py +0 -0
  303. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_ucp_cancel_result.py +0 -0
  304. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
  305. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
  306. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_ucp_profile.py +0 -0
  307. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_update_command.py +0 -0
  308. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/integration/test_wheel_contents.py +0 -0
  309. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
  310. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
  311. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/profile.json +0 -0
  312. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/waitlist.json +0 -0
  313. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/spec_schemas/webhook_event.json +0 -0
  314. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/__init__.py +0 -0
  315. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/fixtures/__init__.py +0 -0
  316. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/fixtures/profiles.py +0 -0
  317. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
  318. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/fixtures/services.py +0 -0
  319. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/__init__.py +0 -0
  320. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/link_cli.py +0 -0
  321. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/mock_calendar.py +0 -0
  322. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/pop_business.py +0 -0
  323. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/mock/pop_transport.py +0 -0
  324. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/support/offline_stack/__init__.py +0 -0
  325. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/__init__.py +0 -0
  326. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_authorization_policy.py +0 -0
  327. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_booking_poll_credentials.py +0 -0
  328. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_booking_request_error.py +0 -0
  329. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_calendar_filter.py +0 -0
  330. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_catalog.py +0 -0
  331. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_check_version.py +0 -0
  332. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_conditional_writes.py +0 -0
  333. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_delivery_address.py +0 -0
  334. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_discovery.py +0 -0
  335. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_fault_reports.py +0 -0
  336. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_geo_eligibility.py +0 -0
  337. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_geocode.py +0 -0
  338. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_harness_docs.py +0 -0
  339. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_headless.py +0 -0
  340. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_http_deadlines.py +0 -0
  341. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_http_retry.py +0 -0
  342. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_live_search_registry.py +0 -0
  343. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_merchant_pacing.py +0 -0
  344. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_mock_support.py +0 -0
  345. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_money_path_events.py +0 -0
  346. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
  347. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_observability.py +0 -0
  348. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_outbound_member_names.py +0 -0
  349. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_package_layering.py +0 -0
  350. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_pay_at_service.py +0 -0
  351. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_provenance.py +0 -0
  352. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_proxy_env.py +0 -0
  353. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_registry_search_filters.py +0 -0
  354. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_registry_search_pricing.py +0 -0
  355. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_registry_search_responses.py +0 -0
  356. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_registry_url.py +0 -0
  357. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_release_notes.py +0 -0
  358. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_rest_client.py +0 -0
  359. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_session_credentials.py +0 -0
  360. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_spec_conformance.py +0 -0
  361. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_step_timing.py +0 -0
  362. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_ucp_capabilities.py +0 -0
  363. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
  364. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
  365. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/unit/test_updates.py +0 -0
  366. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
  367. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
  368. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
  369. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
  370. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
  371. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
  372. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
  373. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
  374. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
  375. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/010-alg-none.json +0 -0
  376. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/README.md +0 -0
  377. {booking_agent_cli-0.11.2 → booking_agent_cli-0.12.0}/tests/vectors/pop/SOURCE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: booking-agent-cli
3
- Version: 0.11.2
3
+ Version: 0.12.0
4
4
  Summary: The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library.
5
5
  Project-URL: Homepage, https://github.com/wix/booking-agent
6
6
  Project-URL: Documentation, https://github.com/wix/booking-agent/blob/master/booking-agent/README.md
@@ -82,7 +82,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
82
82
  the release workflow, so a harness can pin and verify the exact text it follows:
83
83
 
84
84
  ```bash
85
- curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.58.2/booking-agent/skills/booking-agent/SKILL.md
85
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.59.0/booking-agent/skills/booking-agent/SKILL.md
86
86
  gh attestation verify SKILL.md --repo wix/booking-agent \
87
87
  --signer-workflow wix/booking-agent/.github/workflows/release.yml
88
88
  ```
@@ -40,7 +40,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
40
40
  the release workflow, so a harness can pin and verify the exact text it follows:
41
41
 
42
42
  ```bash
43
- curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.58.2/booking-agent/skills/booking-agent/SKILL.md
43
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.59.0/booking-agent/skills/booking-agent/SKILL.md
44
44
  gh attestation verify SKILL.md --repo wix/booking-agent \
45
45
  --signer-workflow wix/booking-agent/.github/workflows/release.yml
46
46
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "booking-agent-cli"
3
- version = "0.11.2"
3
+ version = "0.12.0"
4
4
  description = "The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -3,4 +3,4 @@
3
3
  Ships in the booking-agent-cli wheel and never imports ``booking_agent_cli``.
4
4
  """
5
5
 
6
- __version__ = "0.11.2"
6
+ __version__ = "0.12.0"
@@ -14,7 +14,7 @@ def _parse_iso(s: str) -> datetime:
14
14
  """Parse an ISO8601 datetime string, handling the Z suffix for Python 3.10."""
15
15
  return datetime.fromisoformat(s.replace("Z", "+00:00"))
16
16
 
17
- from typing import Any, Dict, List, Optional
17
+ from typing import Any, Dict, List, Optional, Tuple
18
18
 
19
19
  from ..authorization import BookingScopedCredential
20
20
  from ..discovery import USPBusinessProfile
@@ -79,9 +79,14 @@ class USPBackend(ABC):
79
79
  start_date: str,
80
80
  end_date: str,
81
81
  timezone: Optional[str] = None,
82
+ **booking_questions: Any,
82
83
  ) -> List[TimeSlot]:
83
84
  """Slots for the window. ``timezone`` is sent as ``date_bounds_timezone``.
84
85
 
86
+ ``booking_questions`` carries ``booking_question_assessment_token`` only when
87
+ the booking questions extension is active, so callers that never negotiated
88
+ it send exactly what they did before.
89
+
85
90
  Live backends return an ``AvailabilityResult`` carrying the response's
86
91
  ``business_timezone`` and ``applied_date_bounds_timezone``.
87
92
  """
@@ -122,6 +127,7 @@ class USPBackend(ABC):
122
127
  slot_id: str,
123
128
  service_id: str,
124
129
  spots: int = 1,
130
+ **booking_questions: Any,
125
131
  ) -> Hold: ...
126
132
 
127
133
  @abstractmethod
@@ -147,7 +153,33 @@ class USPBackend(ABC):
147
153
  notes: Optional[str] = None,
148
154
  delivery_address: Optional[Dict[str, Any]] = None,
149
155
  idempotency_key: Optional[str] = None,
150
- ) -> Booking: ...
156
+ **extensions: Any,
157
+ ) -> Booking:
158
+ """``extensions`` carries ``recipient`` and the booking-questions fields, set
159
+ only when they have values."""
160
+
161
+ async def check_eligibility(
162
+ self,
163
+ profile: USPBusinessProfile,
164
+ service_id: str,
165
+ *,
166
+ buyer: Optional[Dict[str, Any]] = None,
167
+ recipient: Optional[Dict[str, Any]] = None,
168
+ submissions: Optional[List[Dict[str, Any]]] = None,
169
+ ) -> Tuple[Any, List[Dict[str, Any]]]:
170
+ raise NotImplementedError("this backend has no eligibility checks")
171
+
172
+ async def update_booking(
173
+ self,
174
+ profile: USPBusinessProfile,
175
+ booking_id: str,
176
+ *,
177
+ credential: Optional[BookingScopedCredential] = None,
178
+ credential_endpoint: Optional[str] = None,
179
+ if_match: Optional[str] = None,
180
+ **changes: Any,
181
+ ) -> Booking:
182
+ raise NotImplementedError("this backend cannot update bookings")
151
183
 
152
184
  @abstractmethod
153
185
  async def get_booking(
@@ -378,6 +378,7 @@ class LiveUSPBackend(USPBackend):
378
378
  start_date: str,
379
379
  end_date: str,
380
380
  timezone: Optional[str] = None,
381
+ **booking_questions: Any,
381
382
  ) -> AvailabilityResult:
382
383
  client = await self._client(profile)
383
384
  return await client.query_availability(
@@ -385,6 +386,7 @@ class LiveUSPBackend(USPBackend):
385
386
  start_date=start_date,
386
387
  end_date=end_date,
387
388
  timezone=timezone,
389
+ **booking_questions,
388
390
  )
389
391
 
390
392
  async def hold_slot(
@@ -393,9 +395,10 @@ class LiveUSPBackend(USPBackend):
393
395
  slot_id: str,
394
396
  service_id: str,
395
397
  spots: int = 1,
398
+ **booking_questions: Any,
396
399
  ) -> Hold:
397
400
  client = await self._client(profile)
398
- hold = await client.hold_slot(slot_id, service_id, spots)
401
+ hold = await client.hold_slot(slot_id, service_id, spots, **booking_questions)
399
402
  self.last_issued_credential = client.last_issued_credential
400
403
  return hold
401
404
 
@@ -422,6 +425,7 @@ class LiveUSPBackend(USPBackend):
422
425
  notes: Optional[str] = None,
423
426
  delivery_address: Optional[Dict[str, Any]] = None,
424
427
  idempotency_key: Optional[str] = None,
428
+ **extensions: Any,
425
429
  ) -> Booking:
426
430
  client = await self._client(profile)
427
431
  booking = await client.create_booking(
@@ -433,12 +437,45 @@ class LiveUSPBackend(USPBackend):
433
437
  notes=notes,
434
438
  delivery_address=delivery_address,
435
439
  idempotency_key=idempotency_key,
440
+ **extensions,
436
441
  )
437
442
  # The only point at which a credential is issued. The caller that owns
438
443
  # session state persists it against (endpoint, booking id).
439
444
  self.last_issued_credential = client.last_issued_credential
440
445
  return booking
441
446
 
447
+ async def check_eligibility(
448
+ self,
449
+ profile: USPBusinessProfile,
450
+ service_id: str,
451
+ *,
452
+ buyer: Optional[Dict[str, Any]] = None,
453
+ recipient: Optional[Dict[str, Any]] = None,
454
+ submissions: Optional[List[Dict[str, Any]]] = None,
455
+ ):
456
+ client = await self._client(profile)
457
+ return await client.check_eligibility(
458
+ service_id, buyer=buyer, recipient=recipient, submissions=submissions,
459
+ )
460
+
461
+ async def update_booking(
462
+ self,
463
+ profile: USPBusinessProfile,
464
+ booking_id: str,
465
+ *,
466
+ credential: Optional[BookingScopedCredential] = None,
467
+ credential_endpoint: Optional[str] = None,
468
+ if_match: Optional[str] = None,
469
+ **changes: Any,
470
+ ) -> Booking:
471
+ client = await self._client(
472
+ profile,
473
+ pop_key_endpoint=credential_endpoint if credential is not None else None,
474
+ )
475
+ return await client.update_booking(
476
+ booking_id, credential=credential, if_match=if_match, **changes,
477
+ )
478
+
442
479
  async def get_booking(
443
480
  self,
444
481
  profile: USPBusinessProfile,
@@ -0,0 +1,482 @@
1
+ """Booking questions: condition evaluation and local answer validation (Section 11.4).
2
+
3
+ Pure functions over the wire dictionaries (``booking_questions.json``). The business
4
+ stays authoritative: local validation only lets the agent re-ask before a request,
5
+ and every rule here is pinned by the vendored ``eval`` and ``wire`` vectors.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import re
12
+ from datetime import date, datetime, timedelta
13
+ from typing import Any, Dict, Iterator, List, Mapping, Optional, Tuple
14
+
15
+ EMAIL_RE = re.compile(r"^[^\s@<>()\[\]\",;:]+@[^\s@<>()\[\]\",;:]+\.[^\s@<>()\[\]\",;:]+$")
16
+ URI_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:[^\s]+$")
17
+ DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
18
+ TIME_RE = re.compile(r"^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$")
19
+ DATE_TIME_RE = re.compile(
20
+ r"^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?"
21
+ r"(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$"
22
+ )
23
+ PHONE_RE = re.compile(r"^\+[1-9]\d{1,14}$")
24
+ DURATION_BOUND_RE = re.compile(r"^(-?)P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?$")
25
+ VALIDITY_RE = re.compile(
26
+ r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
27
+ )
28
+ SUBMISSION_PATH_RE = re.compile(
29
+ r"^\$(?:\.booking)?\.booking_question_submissions\[\?@\.set_id=='(?P<set>[A-Za-z0-9_.-]+)'\]"
30
+ r"(?:\.answers\['(?P<question>[A-Za-z0-9_.-]+)'\])?$"
31
+ )
32
+ BUYER_PATH_RE = re.compile(
33
+ r"^\$(?:\.booking)?\.buyer\.(?P<field>first_name|last_name|email|phone_number)$"
34
+ )
35
+
36
+ ORDERED_OPS = {"gt", "gte", "lt", "lte"}
37
+
38
+ #: Business outcome codes this extension defines (Section 11.4.9).
39
+ ANSWER_CODES = {
40
+ "booking_question_answer_missing",
41
+ "booking_question_answer_invalid",
42
+ "booking_question_unknown",
43
+ "booking_questions_changed",
44
+ "booking_question_update_not_allowed",
45
+ }
46
+ INFO_CODES = {"booking_question_answer_ignored"}
47
+ ELIGIBILITY_CODES = {"booking_eligibility_required", "booking_eligibility_expired"}
48
+ OUTCOME_CODES = ANSWER_CODES | INFO_CODES | ELIGIBILITY_CODES
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # definitions
53
+ # ---------------------------------------------------------------------------
54
+
55
+ def answer_kind(schema: Mapping[str, Any]) -> str:
56
+ """Coarse answer type of a question's answer schema."""
57
+ if schema.get("type") == "array":
58
+ return "multiple_choice"
59
+ if schema.get("type") == "string" and "oneOf" in schema:
60
+ return "single_choice"
61
+ if schema.get("type") in ("integer", "number"):
62
+ return "number"
63
+ if schema.get("type") == "boolean":
64
+ return "acknowledgement" if schema.get("const") is True else "boolean"
65
+ return {"date": "date", "time": "time", "date-time": "date_time"}.get(
66
+ schema.get("format"), "string"
67
+ )
68
+
69
+
70
+ def options_of(schema: Mapping[str, Any]) -> List[Dict[str, Any]]:
71
+ kind = answer_kind(schema)
72
+ if kind == "multiple_choice":
73
+ return list((schema.get("items") or {}).get("anyOf") or [])
74
+ if kind == "single_choice":
75
+ return list(schema.get("oneOf") or [])
76
+ return []
77
+
78
+
79
+ def condition_leaves(condition: Any) -> Iterator[Dict[str, Any]]:
80
+ if not isinstance(condition, dict):
81
+ return
82
+ if "question" in condition:
83
+ yield condition
84
+ for key in ("all", "any"):
85
+ for member in condition.get(key, []):
86
+ yield from condition_leaves(member)
87
+ if "not" in condition:
88
+ yield from condition_leaves(condition["not"])
89
+
90
+
91
+ def semantic_set_errors(qset: Mapping[str, Any]) -> List[str]:
92
+ """Cross-item rules JSON Schema cannot express, as ``rule:item_id`` strings."""
93
+ errors: List[str] = []
94
+ items = qset.get("items", [])
95
+ seen_ids: set = set()
96
+ questions: Dict[str, Dict[str, Any]] = {}
97
+ index_of: Dict[str, int] = {}
98
+ for i, item in enumerate(items):
99
+ if isinstance(item, dict) and isinstance(item.get("id"), str):
100
+ index_of.setdefault(item["id"], i)
101
+ notes = 0
102
+ for i, item in enumerate(items):
103
+ if not isinstance(item, dict):
104
+ continue
105
+ iid = item.get("id")
106
+ if iid in seen_ids:
107
+ errors.append(f"duplicate_item_id:{iid}")
108
+ seen_ids.add(iid)
109
+ kind = item.get("kind")
110
+ if kind == "section":
111
+ following = items[i + 1] if i + 1 < len(items) else None
112
+ if following is None or following.get("kind") == "section":
113
+ errors.append(f"empty_section:{iid}")
114
+ if kind == "question":
115
+ consts = [o.get("const") for o in options_of(item.get("schema", {}))]
116
+ if len(consts) != len(set(consts)):
117
+ errors.append(f"duplicate_option_const:{iid}")
118
+ if item.get("maps_to") == "notes":
119
+ notes += 1
120
+ if notes == 2:
121
+ errors.append(f"multiple_maps_to_notes:{iid}")
122
+ conditions = [item.get("when")]
123
+ if isinstance(item.get("required"), dict):
124
+ conditions.append(item["required"])
125
+ for condition in conditions:
126
+ for leaf in condition_leaves(condition):
127
+ ref = leaf.get("question")
128
+ if ref == iid:
129
+ errors.append(f"self_reference:{iid}")
130
+ elif ref not in index_of:
131
+ errors.append(f"unknown_reference:{iid}")
132
+ elif index_of[ref] > i:
133
+ errors.append(f"forward_reference:{iid}")
134
+ elif ref not in questions:
135
+ errors.append(f"reference_not_question:{iid}")
136
+ else:
137
+ ref_kind = answer_kind(questions[ref].get("schema", {}))
138
+ op = leaf.get("op")
139
+ if op == "contains" and ref_kind != "multiple_choice":
140
+ errors.append(f"operator_type_mismatch:{iid}")
141
+ if op in ORDERED_OPS and ref_kind not in ("number", "date", "time", "date_time"):
142
+ errors.append(f"operator_type_mismatch:{iid}")
143
+ if kind == "question":
144
+ questions[iid] = item
145
+ return errors
146
+
147
+
148
+ MAX_CONDITION_DEPTH = 3
149
+
150
+
151
+ def _condition_depth(condition: Any) -> int:
152
+ if not isinstance(condition, dict) or "question" in condition:
153
+ return 0
154
+ members: List[Any] = list(condition.get("all") or []) + list(condition.get("any") or [])
155
+ if "not" in condition:
156
+ members.append(condition["not"])
157
+ return 1 + max((_condition_depth(m) for m in members), default=0)
158
+
159
+
160
+ def _empty_combinator(condition: Any) -> bool:
161
+ if not isinstance(condition, dict) or "question" in condition:
162
+ return False
163
+ for key in ("all", "any"):
164
+ if key in condition and not condition[key]:
165
+ return True
166
+ members = list(condition.get("all") or []) + list(condition.get("any") or [])
167
+ if "not" in condition:
168
+ members.append(condition["not"])
169
+ return any(_empty_combinator(m) for m in members)
170
+
171
+
172
+ def condition_shape_errors(qset: Mapping[str, Any]) -> List[str]:
173
+ """Condition shapes the schema forbids: combinators nested past depth 3, or empty."""
174
+ errors: List[str] = []
175
+ for item in qset.get("items", []):
176
+ if not isinstance(item, dict):
177
+ continue
178
+ conditions = [item.get("when")]
179
+ if isinstance(item.get("required"), dict):
180
+ conditions.append(item["required"])
181
+ for condition in conditions:
182
+ if condition is None:
183
+ continue
184
+ if _condition_depth(condition) > MAX_CONDITION_DEPTH:
185
+ errors.append(f"condition_depth:{item.get('id')}")
186
+ if _empty_combinator(condition):
187
+ errors.append(f"empty_combinator:{item.get('id')}")
188
+ return errors
189
+
190
+
191
+ def definition_errors(qset: Mapping[str, Any]) -> List[str]:
192
+ """Every reason this client refuses to ask a set (shape and cross-item rules)."""
193
+ return condition_shape_errors(qset) + semantic_set_errors(qset)
194
+
195
+
196
+ def semantic_errors_for(def_name: str, instance: Any) -> List[str]:
197
+ """Semantic errors for any instance that is or carries question sets."""
198
+ if not isinstance(instance, (dict, list)):
199
+ return []
200
+ if def_name == "BookingQuestionSet":
201
+ return semantic_set_errors(instance)
202
+ if def_name == "BookingQuestion" and isinstance(instance, dict):
203
+ return [
204
+ e for e in semantic_set_errors({"items": [instance]})
205
+ if e.startswith("duplicate_option_const:")
206
+ ]
207
+ sets: List[Any] = []
208
+ if def_name == "BookingQuestionSets" and isinstance(instance, list):
209
+ sets = instance
210
+ elif isinstance(instance, dict) and isinstance(instance.get("booking_question_sets"), list):
211
+ sets = instance["booking_question_sets"]
212
+ errors: List[str] = []
213
+ for qset in sets:
214
+ if isinstance(qset, dict):
215
+ errors.extend(semantic_set_errors(qset))
216
+ return errors
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # answers
221
+ # ---------------------------------------------------------------------------
222
+
223
+ def _shift(day: date, sign: int, years: int, months: int, days: int) -> date:
224
+ total = day.year * 12 + day.month - 1 + sign * (years * 12 + months)
225
+ year, month = divmod(total, 12)
226
+ month += 1
227
+ last = (date(year + (month == 12), month % 12 + 1, 1) - timedelta(days=1)).day
228
+ return date(year, month, min(day.day, last)) + timedelta(days=sign * days)
229
+
230
+
231
+ def resolve_date_bound(bound: str, request_date: date) -> date:
232
+ """An absolute date, or a duration relative to the request date (business time zone)."""
233
+ if DATE_RE.match(bound):
234
+ return date.fromisoformat(bound)
235
+ match = DURATION_BOUND_RE.match(bound)
236
+ if not match:
237
+ raise ValueError(f"not a date bound: {bound!r}")
238
+ sign = -1 if match.group(1) else 1
239
+ years, months, weeks, days = (int(g or 0) for g in match.groups()[1:])
240
+ return _shift(request_date, sign, years, months, weeks * 7 + days)
241
+
242
+
243
+ def _parse_instant(text: str) -> datetime:
244
+ return datetime.fromisoformat(text.replace("Z", "+00:00"))
245
+
246
+
247
+ def _valid_date(text: str) -> bool:
248
+ try:
249
+ date.fromisoformat(text)
250
+ except ValueError:
251
+ return False
252
+ return bool(DATE_RE.match(text))
253
+
254
+
255
+ def answer_errors(
256
+ question: Mapping[str, Any], value: Any, request_date: Optional[date] = None,
257
+ ) -> List[str]:
258
+ """Reasons a present answer fails its question, as short slugs."""
259
+ schema = question.get("schema", {})
260
+ kind = answer_kind(schema)
261
+ if kind == "multiple_choice":
262
+ if not isinstance(value, list) or not all(isinstance(v, str) for v in value):
263
+ return ["type"]
264
+ errors = []
265
+ options = {o["const"]: o for o in options_of(schema)}
266
+ if any(v not in options for v in value):
267
+ errors.append("option")
268
+ if len(value) != len(set(value)):
269
+ errors.append("unique")
270
+ if "minItems" in schema and len(value) < schema["minItems"]:
271
+ errors.append("min_items")
272
+ if "maxItems" in schema and len(value) > schema["maxItems"]:
273
+ errors.append("max_items")
274
+ if len(value) > 1 and any(options.get(v, {}).get("exclusive") for v in value):
275
+ errors.append("exclusive")
276
+ return errors
277
+ if kind == "single_choice":
278
+ if not isinstance(value, str):
279
+ return ["type"]
280
+ return [] if value in {o["const"] for o in options_of(schema)} else ["option"]
281
+ if kind in ("boolean", "acknowledgement"):
282
+ if not isinstance(value, bool):
283
+ return ["type"]
284
+ return ["const"] if schema.get("const") is True and value is not True else []
285
+ if kind == "number":
286
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
287
+ return ["type"]
288
+ if schema.get("type") == "integer" and not float(value).is_integer():
289
+ return ["type"]
290
+ errors = []
291
+ if "minimum" in schema and value < schema["minimum"]:
292
+ errors.append("minimum")
293
+ if "maximum" in schema and value > schema["maximum"]:
294
+ errors.append("maximum")
295
+ return errors
296
+ if not isinstance(value, str):
297
+ return ["type"]
298
+ errors = []
299
+ if "minLength" in schema and len(value) < schema["minLength"]:
300
+ errors.append("min_length")
301
+ if "maxLength" in schema and len(value) > schema["maxLength"]:
302
+ errors.append("max_length")
303
+ fmt = schema.get("format")
304
+ if fmt == "email" and not EMAIL_RE.match(value):
305
+ errors.append("format")
306
+ elif fmt == "uri" and not URI_RE.match(value):
307
+ errors.append("format")
308
+ elif fmt == "time" and not TIME_RE.match(value):
309
+ errors.append("format")
310
+ elif fmt == "phone":
311
+ if not PHONE_RE.match(value):
312
+ errors.append("format")
313
+ elif schema.get("allowed_country_codes") and not any(
314
+ value[1:].startswith(code) for code in schema["allowed_country_codes"]
315
+ ):
316
+ errors.append("country_code")
317
+ elif fmt in ("date", "date-time"):
318
+ if fmt == "date" and not _valid_date(value):
319
+ errors.append("format")
320
+ elif fmt == "date-time" and not DATE_TIME_RE.match(value):
321
+ errors.append("format")
322
+ elif request_date is not None:
323
+ day = date.fromisoformat(value[:10])
324
+ if "not_before" in schema and day < resolve_date_bound(schema["not_before"], request_date):
325
+ errors.append("not_before")
326
+ if "not_after" in schema and day > resolve_date_bound(schema["not_after"], request_date):
327
+ errors.append("not_after")
328
+ return errors
329
+
330
+
331
+ # ---------------------------------------------------------------------------
332
+ # conditions
333
+ # ---------------------------------------------------------------------------
334
+
335
+ def _ordered(kind: str, value: Any) -> Any:
336
+ if kind == "date_time":
337
+ return _parse_instant(value)
338
+ if kind == "time":
339
+ return value if len(value) == 8 else f"{value}:00"
340
+ return value
341
+
342
+
343
+ def eval_condition(
344
+ condition: Mapping[str, Any], effective: Mapping[str, Any], questions: Mapping[str, Any],
345
+ ) -> bool:
346
+ """Unanswered or inapplicable references are false; type mismatches are false."""
347
+ if "all" in condition:
348
+ return all(eval_condition(c, effective, questions) for c in condition["all"])
349
+ if "any" in condition:
350
+ return any(eval_condition(c, effective, questions) for c in condition["any"])
351
+ if "not" in condition:
352
+ return not eval_condition(condition["not"], effective, questions)
353
+ ref, op = condition["question"], condition["op"]
354
+ present = ref in effective
355
+ if op == "answered":
356
+ return present
357
+ if not present or ref not in questions:
358
+ return False
359
+ value, operand = effective[ref], condition.get("value")
360
+ kind = answer_kind(questions[ref].get("schema", {}))
361
+ try:
362
+ if op == "equals":
363
+ if kind == "date_time":
364
+ return _parse_instant(value) == _parse_instant(operand)
365
+ return value == operand
366
+ if op == "in":
367
+ return value in operand
368
+ if op == "contains":
369
+ return isinstance(value, list) and operand in value
370
+ left, right = _ordered(kind, value), _ordered(kind, operand)
371
+ return {
372
+ "gt": left > right, "gte": left >= right, "lt": left < right, "lte": left <= right,
373
+ }[op]
374
+ except (TypeError, ValueError, KeyError):
375
+ return False
376
+
377
+
378
+ def evaluate_set(qset: Mapping[str, Any], answers: Mapping[str, Any]) -> Dict[str, List[str]]:
379
+ """Applicability of every item for one set of answers, in item order."""
380
+ questions: Dict[str, Dict[str, Any]] = {}
381
+ effective: Dict[str, Any] = {}
382
+ applicable: List[str] = []
383
+ required_unanswered: List[str] = []
384
+ pruned: List[str] = []
385
+ section_on = True
386
+ for item in qset.get("items", []):
387
+ kind, iid = item.get("kind"), item.get("id")
388
+ when = item.get("when")
389
+ if kind == "section":
390
+ section_on = when is None or eval_condition(when, effective, questions)
391
+ if section_on:
392
+ applicable.append(iid)
393
+ continue
394
+ on = section_on and (when is None or eval_condition(when, effective, questions))
395
+ if kind == "question":
396
+ questions[iid] = item
397
+ if not on:
398
+ if kind == "question" and iid in answers:
399
+ pruned.append(iid)
400
+ continue
401
+ applicable.append(iid)
402
+ if kind != "question":
403
+ continue
404
+ required = item.get("required")
405
+ if isinstance(required, dict):
406
+ required = eval_condition(required, effective, questions)
407
+ if iid in answers:
408
+ effective[iid] = answers[iid]
409
+ elif required:
410
+ required_unanswered.append(iid)
411
+ known = {i.get("id") for i in qset.get("items", []) if i.get("kind") == "question"}
412
+ return {
413
+ "applicable": applicable,
414
+ "required_unanswered": required_unanswered,
415
+ "pruned": pruned,
416
+ "unknown": [k for k in answers if k not in known],
417
+ }
418
+
419
+
420
+ def is_required(item: Mapping[str, Any], qset: Mapping[str, Any], answers: Mapping[str, Any]) -> bool:
421
+ """Whether an applicable question is required given the earlier answers."""
422
+ required = item.get("required")
423
+ if not isinstance(required, dict):
424
+ return bool(required)
425
+ questions: Dict[str, Any] = {}
426
+ effective: Dict[str, Any] = {}
427
+ for other in qset.get("items", []):
428
+ if other.get("id") == item.get("id"):
429
+ break
430
+ if other.get("kind") == "question":
431
+ questions[other["id"]] = other
432
+ if other["id"] in answers:
433
+ effective[other["id"]] = answers[other["id"]]
434
+ return eval_condition(required, effective, questions)
435
+
436
+
437
+ # ---------------------------------------------------------------------------
438
+ # business messages
439
+ # ---------------------------------------------------------------------------
440
+
441
+ def parse_message_path(path: Optional[str]) -> Optional[Tuple[str, Optional[str], Optional[str]]]:
442
+ """``(kind, set_id, question_id)`` for a booking-questions ``messages[].path``.
443
+
444
+ ``kind`` is ``answer`` (question id set), ``set`` (whole set) or ``buyer`` (with
445
+ the buyer field name as ``question_id``). Anything else is None.
446
+ """
447
+ if not path:
448
+ return None
449
+ match = SUBMISSION_PATH_RE.match(path)
450
+ if match:
451
+ question = match.group("question")
452
+ return ("answer" if question else "set", match.group("set"), question)
453
+ match = BUYER_PATH_RE.match(path)
454
+ if match:
455
+ return ("buyer", None, match.group("field"))
456
+ return None
457
+
458
+
459
+ def iso_duration(text: str) -> timedelta:
460
+ match = VALIDITY_RE.match(text)
461
+ if not match:
462
+ raise ValueError(f"not a validity duration: {text!r}")
463
+ y, mo, w, d, h, mi, s = (int(g or 0) for g in match.groups())
464
+ return timedelta(days=y * 365 + mo * 30 + w * 7 + d, hours=h, minutes=mi, seconds=s)
465
+
466
+
467
+ def submissions_digest(submissions: List[Mapping[str, Any]]) -> Optional[str]:
468
+ """Stable digest of what will be sent, or None when nothing is."""
469
+ if not submissions:
470
+ return None
471
+ canonical = json.dumps(
472
+ sorted(submissions, key=lambda s: s.get("set_id", "")),
473
+ sort_keys=True, separators=(",", ":"), ensure_ascii=False,
474
+ )
475
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
476
+
477
+
478
+ def plain_text(value: Any) -> Optional[str]:
479
+ """Plain text of a Description object, or a string as is."""
480
+ if isinstance(value, dict):
481
+ return value.get("plain")
482
+ return value if isinstance(value, str) else None